Applicative functor evaluation is not clear to me

前端 未结 4 456
不思量自难忘°
不思量自难忘° 2020-12-02 01:29

I am currently reading Learn You a Haskell for Great Good! and am stumbling on the explanation for the evaluation of a certain code block. I\'ve read the explanations severa

相关标签:
4条回答
  • 2020-12-02 01:36

    It is using the applicative instance for functions. Your code

    (+) <$> (+3) <*> (*100) $ 5
    

    is evaluated as

    ( (\a->b->a+b) <$> (\c->c+3) <*> (\d->d*100) ) 5
    ( (\x -> (\a->b->a+b) ((\c->c+3) x)) <*> (\d->d*100) ) 5
    ( (\x -> (\a->b->a+b) (x+3)) <*> (\d->d*100) ) 5
    ( (\x -> b -> (x+3)+b) <*> (\d->d*100) ) 5
    ( (\x->b->(x+3)+b) <*> (\d->d*100) ) 5
    (\y -> ((\x->b->(x+3)+b) y) ((\d->d*100) y)) 5
    (\y -> (b->(y+3)+b) (y*100)) 5
    (\y -> (y+3)+(y*100)) 5
    (5+3)+(5*100)
    

    where <$> is fmap or just function composition ., and <*> is ap if you know how it behaves on monads.

    0 讨论(0)
  • 2020-12-02 01:49

    Let us first take a look how fmap and (<*>) are defined for a function:

    instance Functor ((->) r) where
        fmap = (.)
    
    instance Applicative ((->) a) where
        pure = const
        (<*>) f g x = f x (g x)
        liftA2 q f g x = q (f x) (g x)
    

    The expression we aim to evaluate is:

     (+) <$> (+3) <*> (*100)  $ 5
    

    or more verbose:

    ((+) <$> (+3)) <*> (*100) $ 5
    

    If we thus evaluate (<$>), which is an infix synonym for fmap, we thus see that this is equal to:

    (+) . (+3)
    

    so that means our expression is equivalent to:

    ((+) . (+3)) <*> (*100) $ 5
    

    Next we can apply the sequential application. Here f is thus equal to (+) . (+3) and g is (*100). This thus means that we construct a function that looks like:

    \x -> ((+) . (+3)) x ((*100) x)
    

    We can now simplify this and rewrite this into:

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

    and then rewrite it to:

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

    We thus have constructed a function that looks like:

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

    or simpler:

    \x -> 101 * x + 3
    

    If we then calculate:

    (\x -> 101*x + 3) 5
    

    then we of course obtain:

    101 * 5 + 3
    

    and thus:

    505 + 3
    

    which is the expected:

    508
    
    0 讨论(0)
  • 2020-12-02 01:55

    The other two answers have given the detail of how this is calculated - but I thought I might chime in with a more "intuitive" answer to explain how, without going through a detailed calculation, one can "see" that the result must be 508.

    As you implied, every Applicative (in fact, even every Functor) can be viewed as a particular kind of "context" which holds values of a given type. As simple examples:

    • Maybe a is a context in which a value of type a might exist, but might not (usually the result of a computation which may fail for some reason)
    • [a] is a context which can hold zero or more values of type a, with no upper limit on the number - representing all possible outcomes of a particular computation
    • IO a is a context in which a value of type a is available as a result of interacting with "the outside world" in some way. (OK that one isn't so simple...)

    And, relevant to this example:

    • r -> a is a context in which a value of type a is available, but its particular value is not yet known, because it depends on some (as yet unknown) value of type r.

    The Applicative methods can be very well understood on the basis of values in such contexts. pure embeds an "ordinary value" in a "default context" in which it behaves as closely as possible in that context to a "context-free" one. I won't go through this for each of the 4 examples above (most of them are very obvious), but I will note that for functions, pure = const - that is, a "pure value" a is represented by the function which always produces a no matter what the source value.

    Rather than dwell on how <*> can best be described using the "context" metaphor though, I want to dwell on the particular expression:

    f <$> a <*> b
    

    where f is a function between 2 "pure values" and a and b are "values in a context". This expression in fact has a synonym as a function: liftA2. Although using the liftA2 function is generally considered less idiomatic than the "applicative style" using <$> and <*>, the name emphasies that the idea is to "lift" a function on "ordinary values" to one on "values in a context". And when thought of like this, I think it is usually very intuitive what this does, given a particular "context" (ie. a particular Applicative instance).

    So the expression:

    (+) <$> a <*> b
    

    for values a and b of type say f Int for an Applicative f, behaves as follows for different instances f:

    • if f = Maybe, then the result, if a and b are both Just values, is to add up the underlying values and wrap them in a Just. If either a or b is Nothing, then the whole expression is Nothing.
    • if f = [] (the list instance) then the above expression is a list containing all sums of the form a' + b' where a' is in a and b' is in b.
    • if f = IO, then the above expression is an IO action that performs all the I/O effects of a followed by those of b, and results in the sum of the Ints produced by those two actions.

    So what, finally, does it do if f is the function instance? Since a and b are both functions describing how to get a given Int given an arbitrary (Int) input, it is natural that lifting the (+) function over them should be the function that, given an input, gets the result of both the a and b functions, and then adds the results.

    And that is, of course, what it does - and the explicit route by which it does that has been very ably mapped out by the other answers. But the reason why it works out like that - indeed, the very reason we have the instance that f <*> g = \x -> f x (g x), which might otherwise seem rather arbitrary (although in actual fact it's one of the very few things, if not the only thing, that will type-check), is so that the instance matches the semantics of "values which depend on some as-yet-unknown other value, according to the given function". And in general, I would say it's often better to think "at a high level" like this than to be forced to go down to the low-level details of exactly how computations are performed. (Although I certainly don't want to downplay the importance of also being able to do the latter.)

    [Actually, from a philosophical point of view, it might be more accurate to say that the definition is as it is just because it's the "natural" definition that type-checks, and that it's just happy coincidence that the instance then takes on such a nice "meaning". Mathematics is of course full of just such happy "coincidences" which turn out to have very deep reasons behind them.]

    0 讨论(0)
  • 2020-12-02 01:58

    For any applicative,

            a <$> b <*> c  =  liftA2 a b c
    

    For functions,

        liftA2 a  b     c      x 
    =          a (b x) (c x)          -- by definition;
    =       (a . b) x  (c x)
    =     ((a <$> b) <*> c)    x
    

    Thus

            (+) <$> (+3) <*> (*100)  $  5
    =
      liftA2 (+)   (+3)      (*100)     5
    =
             (+)  ((+3) 5)  ((*100) 5)
    =
                  (5+3)  +  (5*100)
    

    (the long version of this answer follows.)

    Pure math has no time. Pure Haskell has no time. Speaking in verbs ("applicative functor applies" etc.) can be confusing ("applies... when?...").

    Instead, (<*>) is a combinator which combines a "computation" (denoted by an applicative functor) carrying a function (in some context) and a "computation" of the same type, carrying a value (in like context), into one combined "computation" that carries out the application of that function to that value (in such context).

    "Computation" is used to contrast it with a pure Haskell "calculations". "Computations" might or might not be pure themselves, that's an orthogonal issue. But mainly what it means, is that "computations" embody a generalized function call protocol. They might "do" something in addition to / as part of / carrying out the application of a function to its argument. Or in types,

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

    With functions, the context is application (another one), and to recover the value -- be it a function or an argument -- the application to a common argument is performed.

    (bear with me, we're almost there). The pattern a <$> b <*> c is also expressible as liftA2 a b c. And so, the "functions" applicative functor "computation" type is defined by

       liftA2 h x y s  = let x' = x s            -- embellished application of h to x and y
                             y' = y s in         -- in context of functions, or Reader
                          h  x'    y'
    
    -- liftA2 h x y    = let x' = x              -- non-embellished application, or Identity
    --                       y' = y   in         
    --                    h  x'    y'
    
    -- liftA2 h x y s  = let (x',s')  = x s      -- embellished application of h to x and y
    --                       (y',s'') = y s' in  -- in context of
    --                   (h  x'    y', s'')      -- state-passing computations, or State
    
    -- liftA2 h x y    = let (x',w)  = x         -- embellished application of h to x and y
    --                       (y',w') = y  in     -- in context of
    --                   (h  x'    y', w++w')    -- logging computations, or Writer
    
    -- liftA2 h x y    = [h  x'    y' |          -- embellished application of h to x and y
    --                             x' <- x,      -- in context of 
    --                             y' <- y ]     -- nondeterministic computations, or List
    
    --  ( and for Monads we define `liftBind h x k =` and replace `y` with `k x'`
    --      in the bodies of the above combinators; then liftA2 becomes liftBind: )
    --    liftA2   ::    (a -> b -> c) -> f a ->       f b  -> f c
    --    liftBind ::    (a -> b -> c) -> f a -> (a -> f b) -> f c
    --    (>>=) = liftBind (\a b -> b) :: f a -> (a -> f b) -> f b
    

    Indeed,

    > :t let liftA2 h x y r = h (x r) (y r) in liftA2
           :: (a -> b -> c)  -> (t -> a)  -> (t -> b)  -> (t -> c)
    
    > :t liftA2   -- the built-in one
    liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
    

    i.e. the types match when we take f a ~ (t -> a) ~ (->) t a, i.e. f ~ (->) t.

    And so, we're already there:

           (+) <$> (+3) <*> (*100)  $ 5
    =
    liftA2 (+)    (+3)      (*100)    5
    =
           (+)   ((+3) 5)  ((*100) 5)
    =
           (+)   (5+3)     (5*100)
    =
                 (5+3)  +  (5*100)
    

    It's just how liftA2 is defined for this type, Applicative ((->) t) => ...:

    instance Applicative ((->) t) where
        pure     x   t =    x
        liftA2 h x y t = h (x t) (y t)
    

    There's no need to define (<*>). The source code says:

    Minimal complete definition

    pure, ((<*>) | liftA2)
    

    So now you've been wanting to ask for a long time, why is it that a <$> b <*> c is equivalent to liftA2 a b c?

    The short answer is, it just is. One can be defined in terms of the other -- i.e. (<*>) can be defined via liftA2,

        g <*> x    = liftA2 id  g     x        -- i.e. (<*>) = liftA2 id = liftA2 ($)
    
    -- (g <*> x) t = liftA2 id  g     x t 
    --               =      id (g t) (x t) 
    --               =    (id . g) t (x t)     -- = (id <$> g <*> x) t
    --               =          g  t (x t)
    

    (which is exactly as it is defined in the source),

    and it is a law that every Applicative Functor must follow, that h <$> g = pure h <*> g.

    Lastly,

    liftA2 h g x  ==  pure h <*> g <*> x
    --     h g x  ==      (h     g)    x
    

    because <*> associates to the left: it is infixl 4 <*>.

    0 讨论(0)
提交回复
热议问题