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: