4

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?

michid
  • 10,536
  • 3
  • 32
  • 59
zichao liu
  • 311
  • 1
  • 6
  • 2
    You've got most of the way there. Haskell has to unify (ie, make equal) the types `Applicative k => k (m -> n)` and `a -> b -> (a, b)` - and can only do this by putting `k` equal to `(a ->)`, `m` to `b` and `n` to `(a, b)`. Then you end up with exactly the signature you started with for `liftA2 (<$>) (,)`. – Robin Zigmond Feb 13 '21 at 16:50
  • 1
    Beta conversion is not used during type inference. No conversion here. Conversion (more properly, reduction) is a way to evaluate a term, trying to reduce it to normal form. Such evaluation is not needed during type inference. Inference roughly uses typing rules and unification, following an algorithm that has its roots in Hindley-Milner. – chi Feb 13 '21 at 17:11
  • 1
    @chi isn't it practically the same thing though, rooted in the Modus Ponens? the "arr" terms `liftA2 = arr( {a->b->c} , {f a -> f b -> f c} ), fmap = arr( {d->e}, {h d -> h e} )` are like lambda terms, then it is like application, `liftA2 fmap = ...` where we "bind" the "parameter" (by unification) and substitute it in the "body" i.e. the second part of the "arr" term. something like that. – Will Ness Feb 13 '21 at 17:51
  • 1
    I've added two tags for you to browse and consult their info pages. – Will Ness Feb 13 '21 at 17:52
  • 1
    This is a very well-written question. You clearly laid out the work you did, why you feel stuck, and what you'd like to learn. Thank you for putting that effort in! – Daniel Wagner Feb 13 '21 at 20:27
  • 1
    To elaborate on @RobinZigmond's comment: `k` unifies with `(a ->)` because the latter is a (applicative) functor (`Reader`) in Haskell. – michid Feb 13 '21 at 22:07

1 Answers1

2

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:

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • 2
    These tables are pretty cool, I should remind myself to use something like that when I need to work out some inference. – pedrofurla Feb 14 '21 at 02:34
  • Thank you very much @Will. I even have a question about your answer. on the inference step `liftA2 (<$>) (,) :: f b -> f c`. I think maybe there should be a `Applicative f =>` or `Applicative (s ->)` constrain? However, even GHCI only constrain `h` by `Functor`, rather than constrain `(s ->)` or `f` by `Applicative`. GHCI turns out `lift2 (<$>) (,) :: Functor k => (a1 -> k a2) -> a1 -> k (a1, a2)`, rather than `lift2 (<$>) (,) :: (Functor k, Applicative (a1 ->))=> (a1 -> k a2) -> a1 -> k (a1, a2)` Why constrain `Applicative (s->) => ` disappeared? – zichao liu Feb 14 '21 at 04:13
  • 1
    because it holds by itself. it does not constraint the resulting `Functor h => ...` type. `Applicative ((->) s)` instance exists for any `s`. we can see it with `:i Applicative` at the prompt in GHCi, and looking through the list of instances it prints we find `instance Applicative ((->) a) -- Defined in \`Control.Applicative'`. It is defined as `(f <*> g) x = f x $ g x` but the definition itself is not needed in type derivations, only the fact that it exists. if there were no such instance, the type checking would stop at that point and report the error, it wouldn't just skip over it. – Will Ness Feb 14 '21 at 05:36
  • 1
    I skipped over this part, implicitly, for simplicity, and kudos for noticing and for asking! another thing is that `s -> t -> (s, t)` = `s -> (t -> (s, t))` = `(->) s ((->) t (s,t))` = `((->) s) ((->) t (s,t))` and thus is naturally *structurally* matches with `f a`. both have two parts to them. (this I also only hinted at, at the answer). – Will Ness Feb 14 '21 at 05:40