Search code examples
haskelltype-inferencelambda-calculusapplicativehindley-milner

How does Haskell perform Beta conversion to derive a type?


I'm learning Haskell by taking fp-course exercise. There is a question block my way. I don't know how Haskell infer lift2 (<$>) (,)'s type, and turn out Functor k => (a1 -> k a2) -> a1 -> k (a1, a2).

I have tried out lift2 (<$>)'s type, and verified by GHCI's command :t lift2 (<$>). step as follow.
I know lift2 :: Applicative k => (a -> b -> c) -> k a -> k b -> k c
I also know (<$>) :: Functor f => (m -> n) -> (f m) -> (f n)
Then by lambda calculus's Beta conversion, I can figure out lift2 (<$>)'s type is
(Applicative k, Functor f) => k (m -> n) -> k (f m) -> k (f n) by replacing a with (m -> n), b with (f m), c with (f n)

When I going to figure out lift2 (<$>) (,)'s type, It block me.
I know (,) :: a -> b -> (a,b)
And lift2 (<$>) :: (Applicative k, Functor f) => k (m -> n) -> k (f m) -> k (f n).
How does Haskell apply lift2 (<$>) to (,)?
The first variable of lift2 (<$>) is Applicative k => k (m -> n).
The to be applied value is (,) :: a -> b -> (a, b)
How the k, m, n replace by a, b?

GHCI's answer is lift2 (<$>) (,) :: Functor k => (a1 -> k a2) -> a1 -> k (a1, a2) by typing :t lift2 (<$>) (,). I cannot infer out this answer by myself.

So I have 2 questions.
1.Could someone show me the inference step by step?
2.In this case the conversion seems not be Beta conversion in lambda calculus (May be I am wrong). What the conversion is?


Solution

  • Type derivation is a mechanical affair.(*) The key is that the function arrow -> is actually a binary operator here, associating on the right (while the application / juxtaposition associates on the left).

    Thus A -> B -> C is actually A -> (B -> C) is actually (->) A ((->) B C) is actually ((->) A) (((->) B) C). In this form it is clear that it consists of two parts so can match up with e.g. f t, noting the equivalences f ~ ((->) A) and t ~ (((->) B) C) (or in pseudocode f ~ (A ->), and also t ~ (B -> C) in normal notation).

    When "applying" two type terms a structural unification is performed. The structures of two terms are matched up, their sub-parts are matched up, and the resulting equivalences are noted as "substitutions" (... ~ ...) available to be performed and ensured in further simplifications of the resulting type terms (and if some incompatibility were to be thus discovered, the type would be then rejected).

    This follows a general structure / type derivation rule rooted in the logical rule of Modus Ponens:

          A -> B     C
         --------------
               B         , where   A ~ C
    

    And thus,

    liftA2 :: A f =>       (   a     -> b   -> c  ) -> f      a         -> f b -> f c
           (<$>) :: F h =>  (d -> e) -> h d -> h e
                 (,) ::                                s -> (t -> (s, t))
    ---------------------------------------------------------------------------------
    liftA2 (<$>) (,) ::                                                    f b -> f c
    ---------------------------------------------------------------------------------
                                      b ~ h d         f ~ (s->)
                            a ~ d->e         c ~ h e        a ~ t->(s,t)
                               \_ _ _ _ _ _ _ _ _ _ _ _ _ _ a ~ d->e
                           ----------------------------------------------------
                                                              d ~ t   e ~ (s,t)
    
    liftA2 (<$>) (,) ::         f     b    -> f     c       
                      ~         (s -> b  ) -> (s -> c      )
                      ~  F h => (s -> h d) -> (s -> h e    )
                      ~  F h => (s -> h t) -> (s -> h (s,t))
    

    (writing A for Applicative and F for Functor, as an abbreviation). The substitutions stop when there are no more type variables to substitute.

    There's some freedom as to which type variables are chosen to be substituted on each step, but the resulting terms will be equivalent up to consistent renaming of the type variables, anyway. For example we could choose

                      ~  F h => (s -> h d) -> (s -> h e    )
                      ~  F h => (s -> h d) -> (s -> h (s,t))
                      ~  F h => (s -> h d) -> (s -> h (s,d))
    

    The Applicative ((->) s) constraint was discovered in the process. It checks out since this instance exists for all s. We can see it by typing :i Applicative at the prompt in GHCi. Looking through the list of instances it prints, we find instance Applicative ((->) a) -- Defined in `Control.Applicative'.

    If there were no such instance the type derivation would stop and report the error, it wouldn't just skip over it. But since the constraint holds, it just disappears as it does not constrain the derived type, Functor h => (s -> h t) -> (s -> h (s,t)). It's already "baked in".

    The instance defines (f <*> g) x = f x $ g x but the definition itself is not needed in type derivations, only the fact that it exists. As for the liftA2, it is defined as

    liftA2 h f g x = (h <$> f <*> g) x   -- for any Applicative (sans the `x`)
                   = (h  .  f <*> g) x   -- for functions
                   = (h . f) x (g x)
                   = f x `h` g x         -- just another combinator
    

    (yes, (<*>) = liftA2 ($) ), so

    liftA2 (<$>) (,) g s = (,) s <$> g s
                         = do { r <- g s       -- in pseudocode, with
                              ; return (s, r)  --  "Functorial" Do
                              }
    

    Or in other words, liftA2 (<$>) (,) = \ g s -> (s ,) <$> g s.

    With the type Functor m => (s -> m t) -> s -> m (s,t). Which is what we have derived.


    (*) See also: