Functions
Lambda abstractions, application, partial application, and the shape of higher-order types. A lambda without usage constraints generalizes to a polymorphic type at its enclosing let binding (or at the top level, if there is no such binding).
identity generalizes to forall a. a -> a
Inferred
Inferred
forall a. a -> a
The identity function has no constraints on x, so its type is quantified over all a. Read forall a. a -> a as "for any type a you pick, this is a function that takes an a and returns an a" — so id can be called on a number and return a number, on a string and return a string, etc. The a is chosen fresh at every use site. The -> arrow reads "takes … returns".
F# test body
X.Fun (X.Ident "x") (X.Var "x")
|> solve [] None
|> shouldSolveType (TDef.Generalize (%0 ^-> %0))
identity applied to String returns String
Inferred
Source
(fun x -> x) "hello"
The companion to the generalization test: now that identity has the polytype forall a. a -> a, applying it to a string should pin a to String at this call. This is the first time we see the instantiation step at work — the generic a gets fixed from the argument type flowing in.
F# test body
X.App (X.Fun (X.Ident "x") (X.Var "x")) (X.Lit "hello")
|> solve [] None
|> shouldSolveType (Mono BuiltinTypes.string)
identity applied to Number returns Number
Inferred
Same instantiation trick, now with a number. Having the two cases side by side (String and Number) is the whole point — it shows that each application picks its own type for a independently, which is what polymorphism promises.
F# test body
X.App (X.Fun (X.Ident "x") (X.Var "x")) (X.Lit 42)
|> solve [] None
|> shouldSolveType (Mono BuiltinTypes.number)
fully applied binary function
Inferred
Env
add : Number -> Number -> Number
Arrows are right-associative: Number -> Number -> Number reads as Number -> (Number -> Number). So add takes a number and returns another function that takes a number and returns a number. Applying both arguments here therefore gives back a Number.
F# test body
X.App (X.App (X.Var "add") (X.Lit 100)) (X.Lit 10)
|> solve
[ "add", Mono (BuiltinTypes.number ^-> BuiltinTypes.number ^-> BuiltinTypes.number) ]
None
|> shouldSolveType (Mono BuiltinTypes.number)
partial application yields a function
Inferred
Env
add : Number -> Number -> Number
Inferred
Number -> Number
Partial application returns a function waiting for the remaining args.
F# test body
X.App (X.Var "add") (X.Lit 100)
|> solve
[ "add", Mono (BuiltinTypes.number ^-> BuiltinTypes.number ^-> BuiltinTypes.number) ]
None
|> shouldSolveType (Mono (BuiltinTypes.number ^-> BuiltinTypes.number))
apply combinator generalizes to (a -> b) -> a -> b
Inferred
Inferred
forall a b. (a -> b) -> a -> b
The apply-combinator. A higher-order function is one that takes another function as an argument (or returns one) — here f is a parameter that gets applied to x. A combinator is just a self-contained function that refers only to its parameters (no outside names). From f x, the inferencer learns that f must accept whatever type x has and produce some return — so f : a -> b, with a and b both free. The whole expression generalizes over both.
F# test body
X.Fun (X.Ident "f")
(X.Fun (X.Ident "x")
(X.App (X.Var "f") (X.Var "x")))
|> solve [] None
|> shouldSolveType (TDef.Generalize ((%0 ^-> %1) ^-> %0 ^-> %1))
double application constrains f to a -> a
Inferred
Source
fun f x -> f (f x)
Inferred
forall a. (a -> a) -> a -> a
Feeding f x back into f forces input and output of f to have the same type — constraining to a -> a.
F# test body
X.Fun (X.Ident "f")
(X.Fun (X.Ident "x")
(X.App (X.Var "f") (X.App (X.Var "f") (X.Var "x"))))
|> solve [] None
|> shouldSolveType (TDef.Generalize ((%0 ^-> %0) ^-> %0 ^-> %0))
function rejects mismatched argument type
Expected error
Env
add : Number -> Number -> Number
Error
Can't unify Number and String
"Unify" is type-checker jargon for "make these two types agree". When the inferencer meets add's signature (second arg: Number) and a string literal at that position, it tries to unify Number with String — and fails, because they're plainly different.
F# test body
X.App (X.App (X.Var "add") (X.Lit 10)) (X.Lit "hello")
|> solve
[ "add", Mono (BuiltinTypes.number ^-> BuiltinTypes.number ^-> BuiltinTypes.number) ]
None
|> shouldFail