We are almost ready to put the pieces together and see how type inference works.
Generalization and Instantiation
The two remaining parts of the puzzle are the operations
generalize which takes a type like a -> a and converts it to the scheme Forall a. a -> a by determining that a is not constrained in the environment, and its dual,
instantiate which takes a general type scheme and replaces the quantified type variables with fresh type variables that correspond to the unknown type parameters at a particular usage site.
Here is the code for the generalize function which simply abstracts a type over all type variables which are free in the type but are not free (and hence, unconstrained) in the given type environment.
> generalize :: TypeEnv -> Type -> Scheme
> generalize env t = Forall as t
> where as = Set.toList $ (freeTvars t) `Set.difference` (freeTvars env)
The instantiation function replaces all bound type variables in a type scheme with fresh (and hence, unconstrained) type variables. Now this slightly tricky; how do we generate a fresh variable that is distinct from all previously generated variables?
We need some state to know what type variables have already generated, and so we will use a MonadState with the following type representing the number of previously generated variables. (Aside: This is exactly like the mlabel function from here)
> data TIState = TIState { count :: Int }
Now, the following action yields a fresh Int
> fresh = do s <- get
> let n = count s
> put $ s { count = n + 1 }
> return n
and we use it to build a function that generates a new type variable with a given prefix
> freshTVar prefix = fresh >>= return . TVar . TV . (prefix ++) . show
Now that we can generate fresh type variables, we can define the instantiate function as
> instantiate (Forall as t) = do as' <- mapM (\ _ -> freshTVar "a") as
> let s = Map.fromList $ zip as as'
> return $ apply s t
Several operations, for example type scheme instantiation, require fresh names for newly introduced type variables. This is implemented by using an appropriate monad which takes care of generating fresh names. It is also capable of passing a dynamically scoped environment, error handling and performing I/O, but we will not go into details here.
Main Type Inference Function
Now, we are ready to pin down the type inference function ti which infers the types for expressions.
> ti :: (MonadState TIState m, MonadError String m) =>
> TypeEnv -> Exp -> m (Subst, Type)
The function expects the precondition that the type environment must contain bindings for all free variables of the expressions. The output action is a state-and-error monad m containing a pair of a substitution which records the type constraints imposed on type variables by the expression, and the inferred type of the expression.
First, the easiest cases are the literals whose base types are trivially inferred.
> ti env (ELit (LInt _)) = return (empSubst, TInt)
> ti env (ELit (LBool _)) = return (empSubst, TInt)
Next, consider the case of variables. Here we simply lookup the environment for the scheme for the variable, and if found, create a fresh instantiation for this particular usage of the variable.
> ti (TypeEnv env) (EVar x) =
> case Map.lookup x env of
> Nothing -> throwError $ "unbound variable: " ++ show x
> Just s -> instantiate s >>= return . (,) empSubst
For the EAbs x e case (ie function definition) we assign the formal x a fresh type variable and analyze the body e using that type variable. Note how the substitution from the body is applied to the type variable for the formal to obtain the inferred function type.
> ti env (EAbs x e) =
> do tv <- freshTVar "a"
> let env' = env \\ (x, Forall [] tv)
> (s1, t1) <- ti env' e
> return (s1, (apply s1 tv) `TArr` t1)
Next, notice how in the EApp e1 e2 case the recorded substitutions for e1 are applied to the environment before e2 is analyzed, and the substitutions from e2 are composed with those for e1 to yield the substitution for the entire expression.
> ti env (EApp e1 e2) =
> do tv <- freshTVar "a"
> (s1, t1) <- ti env e1
> (s2, t2) <- ti (apply s1 env) e2
> s3 <- mgu (apply s2 t1) (TArr t2 tv)
> return (s3 `after` s2 `after` s1, apply s3 tv)
Finally, for the case of a let-binding ELet x e1 e2 we infer the type for e1 and then generalize it to obtain the scheme bound to x when analyzing e2.
> ti env (ELet x e1 e2) =
> do (s1, t1) <- ti env e1
> let t' = generalize (apply s1 env) t1
> env' = env \\ (x, t')
> (s2, t2) <- ti (apply s1 env') e2
> return (s1 `after` s2, t2)
Finally, we use the ti function (which returns a type inference action) to define the typeInference function promised at the beginning.
> -- type TI a = ErrorT String (State TIState) a
> typeInference env e = uncurry apply <$> res
> where act = ti env e
> res = evalState (runErrorT act) s0
> s0 = TIState { count = 0 }
Testing
Here are some simple expressions that we can use to test typeInference
> e0 = ELet (EV "id") (EAbs (EV "x") (EVar (EV "x")))
> (EVar (EV "id"))
>
> e1 = ELet (EV "id") (EAbs (EV "x") (EVar (EV "x")))
> (EApp (EVar (EV "id")) (EVar (EV "id")))
>
> e2 = ELet (EV "id") (EAbs (EV "x") (ELet (EV "y") (EVar (EV "x")) (EVar (EV "y"))))
> (EApp (EVar (EV "id")) (EVar (EV "id")))
>
> e3 = ELet (EV "id") (EAbs (EV "x") (ELet (EV "y") (EVar (EV "x")) (EVar (EV "y"))))
> (EApp (EApp (EVar (EV "id")) (EVar (EV "id"))) (ELit (LInt 2)))
>
> e4 = ELet (EV "id") (EAbs (EV "x") (EApp (EVar (EV "x")) (EVar (EV "x"))))
> (EVar (EV "id"))
>
> e5 = EAbs (EV "m") (ELet (EV "y") (EVar (EV "m"))
> (ELet (EV "x") (EApp (EVar (EV "y")) (ELit (LBool True)))
> (EVar (EV "x"))))
This simple test function tries to infer the type for the given expression. If successful, it prints the expression together with its type, otherwise, it prints the error message.
> test :: Exp -> IO ()
> test e = case typeInference (TypeEnv Map.empty) e of
> Left err -> putStrLn $ "error: " ++ err
> Right t -> putStrLn $ show e ++ " :: " ++ show t
The main program simply infers the types for all the example expressions and prints them together with their inferred types, or prints an error message if type inference fails.
> main :: IO ()
> main = mapM_ test [e0, e1, e2, e3, e4, e5]