Help understanding how pure works

Want to be sure I am understanding this correctly…

> pure (+1) <*> [1..3]
[2,3,4]
  1. So pure is lifting (+1) into the context of Applicative.
  2. [] is already has an instance of Applicative so no lifting needed there.
  3. Pure is looking at the remainder of the expression <*> [1..3] to figure out it needs to lift (+1) into the context of Applicative rather than some other context.
  4. If #3 is correct, what in <*> [1..3] is being looked at to determine that Applicative is the correct context?
1 Like

Here’s a simple way to think about it:

pure does different things depending on what Applicative you use. Here, you’re using the [] Applicative. The pure instance for the [] applicative is

pure x = [x]

Similarly, (<*>) does different things depending on the Applicative. The (<*>) instance for [] is (equivalent to):

fs <*> xs = [f x | f <- fs, x <- xs]

If you plug in the pure definition to

pure (+1) <*> [1..3]

you get

[(+1)] <*> [1,2,3]

and plugging in the <*> definition, you get:

[f x | f <- [(+1)], x <- [1,2,3]]

On rereading your question, I think the point of confusion is point 3. pure doesn’t look into the remainder of the expression. It’s just a function applying to (+1), there’s no way for it to “see” the rest of the expression. (A helpful heuristic: Haskell tends not to have “magic” ways for things to happen like that).

The higher level point here is that to understand what

pure (+1) <*> [1..3]

means, you have to understand how pure and <*> are defined. Those are defined by the [] instance for Applicative. Substitute those definitions in, and you’re good to go.

3 Likes

Also, in case it’s not clear, note that the bracketing is:

(pure (+1)) <*> ([1..3])

That’s why I say that pure doesn’t see anything but (+1)

2 Likes

Though maybe it’s important to note that the compiler does “see” the <*> [1,2,3] in order to infer which Applicative (pure (+1)) must be.

1 Like

Think about your same question but replace Applicative with Functor. Fmap "lifts a function into the context of Functor", as it were. If you had

> fmap (+1) [1..3]
[2,3,4]

would you say "fmap is looking at the remainder of the expression [1..3] to figure out it needs to lift (+1) into the context of Functor rather than some other context"? No, of course not, because fmap always lifts functions into "the context of Functor", and you know that by looking at its type signature! But there’s actually some more confusion here, because it’s not really "the context of Functor"; let me explain.

Applicative is a typeclass, just like Functor, and so there are multiple types which have Applicative instances. If you have some confusion because the source you’re learning from talks about how pure can lift something into different contexts, what they mean is that the different types which are instances of Applicative are different contexts. That is, [a] is a different context than Maybe a, which is a different context than IO a, which is a different context than Either String a, and so on, and those are all Applicatives (and Functors, and Monads for that matter). pure will always “lift into an Applicative context”, of which there are more than one. But you can be sure that it will always be an instance of Applicative, because it’s guaranteed by the type signature of pure.

So, it just looks to me like your main confusion is in thinking that pure can lift something into a context other than something involving Applicative (which again, is a typeclass, not a type itself). pure will always lift into some kind of Applicative context, meaning, some type which has an Applicative instance. So it’s a subtle point about language: it’s not "the context of Applicative", it’s “an Applicative context,” of which, again, there are many.

1 Like

Now that you mention it, it is obvious I should have said compiler- thx!

Good point! I kind of knew that but I see the distinction more clearly now.

Very helpful! Speaking of the definition of the [] instance for Applicative:

instance Applicative [] where
    {-# INLINE pure #-}
    pure x    = [x]
    {-# INLINE (<*>) #-}
    fs <*> xs = [f x | f <- fs, x <- xs]
    {-# INLINE liftA2 #-}
    liftA2 f xs ys = [f x y | x <- xs, y <- ys]
    {-# INLINE (*>) #-}
    xs *> ys  = [y | _ <- xs, y <- ys]

Can you explain the left hand side of fs <*> xs = [f x | f <- fs, x <- xs], i.e. fs <*> xs?

  1. It looks like pattern matching on an operator - is that a thing?

  2. I’ll guess that fs and xs both need to be []? If so, what enforces that?

  3. And there is nothing beyond enforcing [], i.e., you could put anything in there?

Happy to help!

Re 1: it’s just a normal function definition (but for an operator). I don’t think it’s pattern matching. For example, in Haskell, I could define a new operator like this:

(+++) :: Int -> Int -> Int 
a +++ b = a + b + 1

Does that make sense?

Re 2: it’s important to be very precise here. fs isn’t of type [] exactly, it’s of type [a]. Same for xs. There’s a few things you could mean by “what enforces that?”, but one answer is: since we’re writing the Applicative instance for [], that’s what enforces it.

Re 3: no. In this line, the type of <*> is [a ->b] -> [a] -> [b], so you could not put just anything here.

2 Likes

Note that you can also write it in prefix style:

(<*>) fs xs = [f x | f <- fs, x <- xs]

Then you can just consider (<*>) to be the name of the function.

2 Likes

#1
I’m looking at Base.hs and don’t see a type signature there so missed that. In VS Code HLS says the type signature is (GHCi gives a parse error)

(<*>) :: [t -> a] -> [t] -> [a]
fs <*> xs = [f x | f <- fs, x <- xs]

Interesting that fs & xs are not on the left hand side. Seems like a big hint that an operator is being defined. I have never defined an operator so just getting a feel for it now.

And if I reorder the lhs

(<*>) :: [t -> a] -> [t] -> [a]
(<*>) fs xs = [f x | f <- fs, x <- xs]

I’m looking at the doc and in GHC code but don’t see a similar type signature. There is none near the declaration for instance Applicative [] where Is it there or maybe inferred?

#2 & 3 - good with those now.

I did see that and it does make it easier to understand.

Re 1. The “infix” notation (i.e. putting the operator between the two arguments, like in 3 + 4) is totally a syntactic thing. So if confused, always just rearrange 3 + 4 to (+) 3 4 (note that Haskell needs brackets for the + when it’s at front). Similarly, there’s nothing deep about a definition like fs <*> xs = ...: it’s just a totally normal function definition, which is why jaror recommended you rewrite it as (<*>) fs xs

You said that fs and xs aren’t on the left hand side. But they are! In fs <*> xs = ..., that’s fs and xs on the left hand side. Unless I misunderstood what you were saying.

It sounds like you’re looking for a definition of the form (<*>) fs xs = ... somewhere? That definition is fs <*> xs = [f x | f <- fs, x <- xs] (it’s identical! just two different ways of writing the same thing)

1 Like

Just to be totally clear: if I wanted to define a new function blah, e.g. of type Bool -> Bool -> Bool, I could either define it like:

a `blah` b = if a then b else a -- or whatever definition I wanted on the right

or

blah a b = if a then b else a -- or whatever

It’s literally just two syntaxes for exactly the same function. In the instance definition of the [] applicative, they happen to use the infix version (the first one above), but they could have just as well used the second.

1 Like

I wasn’t clear. I was speaking of the type signature (<*>) :: [t -> a] -> [t] -> [a] when I said not on the lhs.

When you said “the type of <*> is [a ->b] -> [a] -> [b] , so you could not put just anything here.” I went looking for the type signature in the doc and code but don’t see it.

Ah sorry, I misunderstood. Yeah, you can work out the signature from the signature of <*> given in the Applicative typeclass, which is:

class Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

So when f is specialized to [], we get (<*>) :: [] (a -> b) -> [] a -> [] b which is just another way of writing (<*>) :: [(a -> b)] -> [a] -> [b]

Hope that’s useful!

2 Likes