0
votes

I've recently been trying to learn Haskell with the "Learn You a Haskell" and have been really struggling with understanding functions as Applicatives. I should point out that using other types of Applicatives like Lists and Maybe I seem to understand well enough to use them effectively.

As I tend to do when trying to understand something is I tried to play with as many examples as I could and once the pattern emerges things tend to make sense. As such I tried a few examples. Attached are my notes of several examples I tried along with a diagram I drew to try to visualize what was happening.

enter image description here

The definition of funct doesnt seem to relevant to the outcome but in my tests I used a function with the following definition:

funct :: (Num a) => a -> a -> a -> a

At the bottom I tried to show the same thing as in the diagrams just using normal math notation.

So all of this is well and good, I can understand the pattern when I have some function of an arbitrary number of arguments (though needs 2 or more) and apply it to a function that takes one argument. However intuitively this pattern doesn't make that much sense to me.

So here are the specific questions I have:

What is the intuitive way to understand the pattern I'm seeing, particularly if i view an Applicative as a container (which is how I view Maybe and lists)?

What is the pattern when the function on the right of the <*> takes more than a single argument (I've mostly been using the function (+3) or (+5) on the right)?

why is the function on the right hand side of the <*> applied to the second argument of the function on the left side. For example if the function on the right hand side were f() then funct(a,b,c) turns into funct (x, f(x), c)?

Why does it work for funct <*> (+3) but not for funct <*> (+)? Moreover it DOES work for (\ a b -> 3) <*> (+)

Any explanation that gives me a better intuitive understanding for this concept would be greatly appreciated. I read other explanations such as in the book I mentioned that explains functions in terms of ((->)r) or similar patterns but even though I know how to use the ->) operator when defining a function I'm not sure i understand it in this context.

Extra Details:

I want to also include the actual code I used to help me form the diagrams above.

First I defined funct as I showed above with:

funct :: (Num a) => a -> a -> a -> a

Throughout the process i refined funct in various ways to understand what was going on.

Next I tried this code:

funct a b c = 6 
functMod =  funct <*> (+3)
functMod 2 3

Unsuprisingly the result was 6

So now I tried just returning each argument directly like this:

funct a b c = a
functMod =  funct <*> (+3)
functMod 2 3 -- returns 2

funct a b c = b
functMod =  funct <*> (+3)
functMod 2 3 -- returns 5

funct a b c = c
functMod =  funct <*> (+3)
functMod 2 3 -- returns 3

From this I was able to confirm the second diagram is what was taking place. I repeated this patterns to observe the third diagram as well (which is the same patterns extended on top a second time).

3
I'm struggling to understand what you're really asking about. Even if the definition of funct isn't relevant, can you at least edit the question so that you tell us its type?Robin Zigmond
My question was basically just looking for understanding, but i added specific questions below.. I will add a definition for funct though. and if you can think of anyway I can add additional clarity please let me know.Jeffrey Phillips Freeman
@RobinZigmond I've updated the question per your suggestion. Is this better or is there any other way I could help clarify what I'm asking?Jeffrey Phillips Freeman
I'm afraid that doesn't really clear anything up. We need to see some actual code you've tried to run, together with a description of what you expected, and what the actual result was instead.Robin Zigmond
@RobinZigmond Alright, give me a minute ill share all the bits of the code I used to draw the diagram above in the first place.Jeffrey Phillips Freeman

3 Answers

2
votes

You can usually understand what a function is doing in Haskell if you substitute its definition into some examples. You already have some examples and the definition you need is <*> for (->) a which is this:

(f <*> g) x = f x (g x)

I don't know if you'll find any better intuition than just using the definition a few times.

On your first example we get this:

  (funct <*> (+3)) x
= funct x ((+3) x)
= funct x (x+3)

(Since there was nothing I could do with funct <*> (+3) without a further parameter I just applied it to x - do this any time you need to.)

And the rest:

  (funct <*> (+3) <*> (+5)) x
= (funct x (x+3) <*> (+5)) x
= funct x (x+3) x ((+5) x)
= funct x (x+3) x (x+5)

  (funct <*> (+)) x
= funct x ((+) x)
= funct x (x+)

Notice you can't use the same funct with both of these - in the first it can take four numbers, but in the second it needs to take a number and a function.

  ((\a b -> 3) <*> (+)) x
= (\a b -> 3) x (x+)
= (\b -> 3) (x+)
= 3

  (((\a b -> a + b) <*> (+)) x
= (\a b -> a + b) x (x+)
= x + (x+)
= type error
2
votes

As pointed out by David Fletcher, (<*>) for functions is:

(g <*> f) x = g x (f x)

There are two intuitive pictures of (<*>) for functions which, though not quite able to stop it from being dizzying, might help with keeping your balance as you go through code that uses it. In the next few paragraphs, I will use (+) <*> negate as a running example, so you might want to try it out a few times in GHCi before continuing.

The first picture is (<*>) as applying the result of a function to the result of another function:

g <*> f = \x -> (g x) (f x)

For instance, (+) <*> negate passes an argument to both (+) and negate, giving out a function and a number respectively, and then applies one to the other...

(+) <*> negate = \x -> (x +) (negate x)

... which explains why its result is always 0.

The second picture is (<*>) as a variation on function composition in which the argument is also used to determine what the second function to be composed will be

g <*> f = \x -> (g x . f) x

From that point of view, (+) <*> negate negates the argument and then adds the argument to the result:

(+) <*> negate = \x -> ((x +) . negate) x

If you have a funct :: Num a => a -> a -> a -> a, funct <*> (+3) works because:

  • In terms of the first picture: (+ 3) x is a number, and so you can apply funct x to it, ending up with funct x ((+ 3) x), a function that takes two arguments.

  • In terms of the second picture: funct x is a function (of type Num a => a -> a -> a) that takes a number, and so you can compose it with (+ 3) :: Num a => a -> a.

On the other hand, with funct <*> (+), we have:

  • In terms of the first picture: (+) x is not a number, but a Num a => a -> a function, and so you can't apply funct x to it.

  • In terms of the second picture: the result type of (+), when seen as a function of one argument ((+) :: Num a => a -> (a -> a)), is Num a => a -> a (and not Num a => a), and so you can't compose it with funct x (which expects a Num a => a).

For an arbitrary example of something that does work with (+) as the second argument to (<*>), consider the function iterate:

iterate :: (a -> a) -> a -> [a]

Given a function and an initial value, iterate generates an infinite list by repeatedly applying the function. If we flip the arguments to iterate, we end up with:

flip iterate :: a -> (a -> a) -> [a]

Given the problem with funct <*> (+) was that funct x wouldn't take a Num a => a -> a function, this seems to have a suitable type. And sure enough:

GHCi> take 10 $ (flip iterate <*> (+)) 1
[1,2,3,4,5,6,7,8,9,10]

(On a tangential note, you can leave out the flip if you use (=<<) instead of (<*>). That, however, is a different story.)


As a final aside, neither of the two intuitive pictures lends itself particularly well to the common use case of applicative style expressions such as:

(+) <$> (^2) <*> (^3)

To use the intuitive pictures there, you'd have to account for how (<$>) for functions is (.), which murks things quite a bit. It is easier to just see the entire thing as lifted application instead: in this example, we are adding up the results of (^2) and (^3). The equivalent spelling as...

liftA2 (+) (^2) (^3)

... somewhat emphasises that. Personally, though, I feel one possible disadvantage of writing liftA2 in this setting is that, if you apply the resulting function right in the same expression, you end up with something like...

liftA2 (+) (^2) (^3) 5

... and seeing liftA2 followed by three arguments tends to make my brain tilt.

1
votes

You can view the function monad as a container. Note that it's really a separate monad for every argument-type, so we can pick a simple example: Bool.

type M a = Bool -> a

This is equivalent to

data M' a = M' { resultForFalse :: a
               , resultForTrue :: a  }

and the instances could be defined

instance Functor M where            instance Functor M' where
  fmap f (M g) = M g'                 fmap f (M' gFalse gTrue) = M g'False g'True
   where g' False = f $ g False        where g'False = f $ gFalse
         g' True  = f $ g True               g'True  = f $ gTrue

and similar for Applicative and Monad.

Of course this exhaustive case-listing definition would become totally impractical for argument-types with more than a few possible values, but it's always the same principle.

But the important thing to take away is that the instances are always specific for one particular argument. So, Bool -> Int and Bool -> String belong to the same monad, but Int -> Int and Char -> Int do not. Int -> Double -> Int does belong to the same monad as Int -> Int, but only if you consider Double -> Int as an opaque result type which has nothing to do with the Int-> monad.

So, if you're considering something like a -> a -> a -> a then this is not really a question about applicatives/monads but about Haskell in general. And therefore, you shouldn't expect that the monad=container picture gets you anywhere. To understand a -> a -> a -> a as a member of a monad, you need to pick out which of the arrows you're talking about; in this case it's only the leftmost one, i.e. you have the value M (a->a->a) in the type M=(a->) monad. The arrows between a->a->a do not participate in the monadic action in any way; if they do in your code, then it means you're actually mixing multiple monads together. Before you do that, you should understand how a single monad works, so stick to examples with only a single function arrow.