Wednesday, May 2, 2012

Printing types

The following exercise consists in write a function that prints the type of a given monomorphic function. No, we are not talking about type-inference. We will use the one that Haskell brings. Anyway, the task is to define a function writeType that, given some Haskell value (which can be a function), print its type on the screen.

It's not a hard exercise, so it can be tried by a beginner that already knows how to work with Haskell types. Try it before read the solution.

This is also my first attempt to create a blog post with Pancod and HsColour from a literate Haskell source code.

Solution

The approach of the solution is similar (identical) to the one taken in the Data.Typeable base module. We will import the intersperse function, which will be useful when defining a pretty-printer for types.

importData.List(intersperse)

Our first mission is a function that, for some type a, returns its type signature. Something like typeOf :: a -> Type. But we need to first define the Type datatype.

And that's the way for types of null arity. Note that we always discard arguments. This is necessary, because we really need to avoid depending in values. We will have problems if the compiler tries to reduce some expression. Even it would be nonsensical, because the type of a value has nothing to do with one of its values.

Now, let's define our first type trick. If we want to define instances for types with positive arity, we will need to apply typeOf with argument(s) of the inner(s) type(s).

Here is provided the deconstructor for types with arity one.

decons::ta->adecons=undefined

As you can see, it is not actually defined. All we need is to use its type, so the definition does not matter. Note why we did not want of typeOf to try to evaluate its argument.

The good thing is that all types are traversed recursively. For example, with typeOf (1,Just 2), it's reduced to TTuple [typeOf 1,typeOf (Just 2)], then to TTuple [TCons "Int" [], TCons "Maybe" [typeOf 2]], and finally to TTuple [TCons "Int" [], TCons "Maybe" [TCons "Int" []]]. Well, this evaluation is not true, but it works like that (replacing with undefineds everywhere!). What does this work is the Haskell type system. We are only playing with types, never with values.

The last instance we will do is for the function type constructor. Though, if you think about it, there is not something new. The arrow -> is just a type constructor with arity 2.

However, our problem does not end here (though here ends the most interesting part). The problem was to print the type of a given function. The next step is to write a pretty-printer function for types.

First, it will be handy to have a function that tell us if a type will need to be parenthesized when appears as an argument for some type constructor. For example, Int -> Int in Maybe (Int -> Int).

An argument of an applied type constructor only will need to be parenthesized when its arity is not null. A function always will need it (because it's a constructor with arity two). No other will thanks to the syntax of tuples and list types. They are already parenthesized in some way.

However if the type constructor is the arrow -> the parenthesis are only needed when the left argument is a function type, since is infix and right-associative. For example, Maybe Float -> (Float -> Float) does not need parenthesis (I put them to make clear the association order), but (Maybe Float -> Float) -> Float needs them. Let's define then a function that test if a type is functional.

isFun::Type->BoolisFun(TFun__)=TrueisFun_=False

To surround an expression with parenthesis we define the par function.

par::String->Stringparstr=concat["(",str,")"]

It's time for our printType :: Type -> String function. For expressions that must be parenthesized when needed we will use the variant printTypeIf. It will put parenthesis when a test function holds.