11

From the chapter on Functors in Learn You a Haskell for Great Good, Lipovača states:

"When we do (+) <$> (+3) <*> (*100), we're making a function that will use + on the results of (+3) and (*100) and return that. To demonstrate on a real example, when we did (+) <$> (+3) <*> (*100) $ 5, the 5 first got applied to (+3) and (*100), resulting in 8 and 500. Then, + gets called with 8 and 500, resulting in 508."

However, if I try to evaluate the function myself, considering this definition for Applicative on the functor ((->) r):

instance Applicative ((->) r) where  
    pure x = (\_ -> x)  
    f <*> g = \x -> f x (g x)  

I read the evaluation of the above expression as:

(\x -> (3 + x) (100 * x)) $ 5

But I don't see how we can compose two partially applied binary functions as a single lambda (in fact, GHCi throws an infinite type error trying to bind this to a variable). Furthermore, to a working interpretation, if we look at the type definition for <$> we get:

(<$>) :: Functor f => (a -> b) -> f a -> f b

or more specifically we can look at its lifting as:

(<$>) :: Functor f => (a -> b) -> (f a -> f b)

Considering that our functor in this case is ((->) r), I can deduce that this is what transformation takes place on the previous evaluation (assuming that left associativity happens first, instead of the right associative application of 5):

(\x -> a + b) where a = (+ 3) and b = (* 100). This is the function that should be returned. However, am I correct in assuming that this is the final (rough) form?

(\x -> (3 + x) + (100 * x)) $ 5

...which yields 508.

I find Lipovača's description more comprehensible in terms of how the expression works, but my gut tells me isn't entirely true for the gorey details under the Haskell compiler hood. It is easier for me to think that the fmap of (+) happened first resulting in a function with two functors who are partially applied functions that take a shared input, and then we applied a value to it. We can do this because of lazy evaluation. Is this wrong?

rjs
  • 838
  • 2
  • 10
  • 21

1 Answers1

16

Firstly, note that both <$> and <*> associate to the left. There's nothing magical happening internally and we can see the transformation with essentially a series of eta expansions and beta reductions. Step-by-step, it looks like this:

(((+) <$> (+3))         <*> (*100)) $ 5        -- Add parens
((fmap (+) (+3))        <*> (*100)) $ 5        -- Prefix fmap
(((+) . (+3))           <*> (*100)) $ 5        -- fmap = (.)
((\a -> (+) ((+3) a))   <*> (*100)) $ 5        -- Definition of (.)
((\a -> (+) (a+3))      <*> (*100)) $ 5        -- Infix +
((\a b -> (+) (a+3) b)) <*> (*100)) $ 5        -- Eta expand
(\x -> (\a b -> (+) (a+3) b) x ((*100) x)) $ 5 -- Definition of (<*>)
(\x -> (\a b -> (+) (a+3) b) x (x*100)) $ 5    -- Infix *
(\a b -> (+) (a + 3) b) 5 (5*100)              -- Beta reduce
(\a b -> (a + 3) + b)   5 (5*100)              -- Infix +
(5 + 3) + (5*100)                              -- Beta reduce (twice)
508                                            -- Definitions of + and *

A bit confusingly, the fact that $ associates to the right has less to do with what's happening here than the fact that its fixity is 0. We can see this if we define a new operator:

(#) :: (a -> b) -> a -> b
f # a = f a
infixl 0 #

and in GHCi:

λ> (+) <$> (+3) <*> (*100) # 5
508
David Young
  • 10,713
  • 2
  • 33
  • 47
  • Thanks a ton for this, this is exactly what I was looking for. I'll have to look more into Eta and Beta expansion and reduction for future reading. Even though `$` is right associative, it's being applied to a thunk (is it safe to call all the mapping happening left of the $ a thunk?) and therefore all of this gets evaluated, right? – rjs Jul 01 '15 at 04:08
  • @RJS You can think of `f $ a` "acting like" `(f) (a)` where `f` and `a` are bits of code that can have spaces (it's not quite like that all the time though, especially when `$`s are chained, but I often think of it like that). I added a little bit about fixity, I should probably expand on it. There is actually a (pretty good) argument for making `$` left associative, incidentally. I'm not sure how being a thunk is related. What do you mean by that? – David Young Jul 01 '15 at 04:12
  • Ah just wrong semantics on my part; a poor assumption that all unevaluated arguments are thunks is all. – rjs Jul 01 '15 at 04:19
  • 1
    @RJS Well, it is correct that an unevaluated argument is a thunk (unless it is optimized out altogether, maybe). The fact that something is a thunk generally only matters when looking at performance, infinitely large structures, infinite loops or expressions that generate errors. In particular, in the absence of infinite things and errors/exceptions, thunks don't change the meaning of something (outside of performance characteristics). – David Young Jul 01 '15 at 04:21
  • 1
    By the way, eta expansion/reduction and beta reduction are terms from lambda calculus. Essentially, eta expansion means taking an expression `f` (where `f` is a function) and turning it into `\x -> f x`. Eta reduction goes the other way. Beta reduction is reducing a function application: `(\x -> f x) a` gets turned into `f a`. There are subtleties related to variable capture, but that is most important when making a compiler or interpreter. It's just important to remember that, when you're doing it by hand, you should use fresh variables so that name conflicts don't occur. – David Young Jul 01 '15 at 04:27
  • I see where I was lead astray; step six and seven on your chart expand the curried variable into an explicit one (which I was forgetting). Since this is a great answer, maybe adding one more 'infix' step for the prefixed `(+)` around step nine might be useful considering that you include infix steps for other parts...just a thought. Thanks again! – rjs Jul 01 '15 at 04:50
  • The first "Eta expand" step also snuck in a "definition of (.)", which you may want to make a separate step. – Rein Henrichs Jul 01 '15 at 19:07
  • @ReinHenrichs Good catch. That should say "Definition of (.)" – David Young Jul 02 '15 at 05:31