(skip down for a manual derivation)
Find the type of head . filter fst
== ((.) head) (filter fst)
, given
head :: [a] -> a
(.) :: (b -> c) -> ((a -> b) -> (a -> c))
filter :: (a -> Bool) -> ([a] -> [a])
fst :: (a, b) -> a
This is achieved in a purely mechanical manner by a small Prolog program:
type(head, arrow(list(A) , A)). %% -- known facts
type(compose, arrow(arrow(B, C) , arrow(arrow(A, B), arrow(A, C)))).
type(filter, arrow(arrow(A, bool), arrow(list(A) , list(A)))).
type(fst, arrow(pair(A, B) , A)).
type([F, X], T):- type(F, arrow(A, T)), type(X, A). %% -- application rule
which automagically produces, when run in a Prolog interpreter,
3 ?- type([[compose, head], [filter, fst]], T).
T = arrow(list(pair(bool, A)), pair(bool, A)) %% -- [(Bool,a)] -> (Bool,a)
where types are represented as compound data terms, in a purely syntactical manner. E.g. the type [a] -> a
is represented by arrow(list(A), A)
, with possible Haskell equivalent Arrow (List (Logvar "a")) (Logvar "a")
, given the appropriate data
definitions.
Only one inference rule, that of an application, was used, as well as Prolog's structural unification whereby compound terms match if they have the same shape and their constituents match: f(a1, a2, ... an) and g(b1, b2, ... bm) match iff f is the same as g, n == m and ai matches bi, with logical variables being able to take on any value as needed, but only once (can't be changed).
4 ?- type([compose, head], T1). %% -- (.) head :: (a -> [b]) -> (a -> b)
T1 = arrow(arrow(A, list(B)), arrow(A, B))
5 ?- type([filter, fst], T2). %% -- filter fst :: [(Bool,a)] -> [(Bool,a)]
T2 = arrow(list(pair(bool, A)), list(pair(bool, A)))
To perform type inference manually in a mechanical fashion, involves writing things one under another, noting equivalences on the side and performing the substitutions thus mimicking the operations of Prolog. We can treat any ->, (_,_), []
etc. purely as syntactical markers, without understanding their meaning at all, and perform the process mechanically using structural unification and, here, only one rule of type inference, viz. rule of application: (a -> b) c ⊢ b {a ~ c}
(replace a juxtaposition of (a -> b)
and c
, with b
, under the equivalence of a
and c
). It is important to rename logical variables, consistently, to avoid name clashes:
(.) :: (b -> c ) -> ((a -> b ) -> (a -> c )) b ~ [a1],
head :: [a1] -> a1 c ~ a1
(.) head :: (a ->[a1]) -> (a -> c )
(a ->[c] ) -> (a -> c )
---------------------------------------------------------
filter :: ( a -> Bool) -> ([a] -> [a]) a ~ (a1,b),
fst :: (a1, b) -> a1 Bool ~ a1
filter fst :: [(a1,b)] -> [(a1,b)]
[(Bool,b)] -> [(Bool,b)]
---------------------------------------------------------
(.) head :: ( a -> [ c ]) -> (a -> c) a ~ [(Bool,b)]
filter fst :: [(Bool,b)] -> [(Bool,b)] c ~ (Bool,b)
((.) head) (filter fst) :: a -> c
[(Bool,b)] -> (Bool,b)