Reading type annotations

C and C-style languages like C++, Java, and C# tend to have method types written like this:

returnType methodName(argType0 arg0, argType1 arg1);

Other typed languages and programming papers use a notation more like this:

methodName : (argType0, argType1) -> returnType

I found it took a bit of getting used to, but I now much prefer to read and write this style. I think it is worth becoming familiar with, as it is used in quite a few languages1 and in all the programming papers I’ve seen. So here’s a quick guide on how to read this style of type annotation.

Structure

From the methodName example above, we can see the structure has changed from “return type - name - arguments” to “name - arguments - return type”. So the main change is moving the return type from the beginning to the end.

A : separates the name from the type signature. : can be read as “has type”. Haskell unfortunately uses :: for this, instead of the : character which seems to be used pretty much everywhere else.

A -> arrow separates function input from function output. So a -> b reads as “I take values of type a and produce values of type b”.

Arguments are shown as a tuple of one or more types. In some languages (like ML, OCaml, and F#) tuple types are shown denoted by types separated by * characters, so the signature would look like methodName : argType0 * argType1 -> returnType.

Generics

There are a few different ways of representing generic parameters. Let’s take a function that, given a single element of some type, returns a singleton list of that type.

In Haskell, any type starting with a lowercase character is a type variable rather than a concrete type. In F# type parameters begin with a quote character '. Not requiring an additional step to list generic parameters is handy.

Higher order functions

Where this notation starts to show some advantages is with higher order functions. For example, say we want a generic map function:

These functions take a function that translates Ts to As, and a list of Ts, to produce a list of As. The parentheses around the (t -> a) in the Haskell-style signature show that this is a single argument (that happens to itself be another function). This is a bit cleaner than the equivalent Func<T, A> in the C# version, particularly when the explicit type parameter declarations are taken into account. The difference becomes more noticeable as we increase the number of functions and type parameters:

Curried functions

map1 takes a function (t -> a) and returns a function List t -> List a. It would also be correct to write it as map1 :: (t -> a) -> (List t -> List a). In constrast, map2 takes a single argument that happens to be a tuple of ((t -> a), List t). If we are supplying both arguments at once there is not much difference, but the map1 version also lets us supply just the (t -> a) argument to create a new function.

Being able to supply less than the full contingent of arguments to a function, and get back a function that takes the remainder of the arguments, is called partial application.

The map1 form of signature, where a function is built out of functions that each take a single argument, is called a curried function (map2 is “uncurried”). We get partial application, the ability to provide one argument at a time, for free with curried functions.

Unit values

Some methods take no input and return a value (either a constant, or due to some side-effect). The “no input” value is normally represented by empty parenthesis (), and is called “unit” (because there is only a single legal value of this type, ()).

DateTime GetDate();
getDate : () -> DateTime

Similarly for things that take an argument but produce no direct output value (i.e. performs a side-effect)2. Again, this is represented by unit:

void Save(Widget w);
save : Widget -> ()

This starts to look a bit funny when methods take other calls with no input and no direct output:

void Subscribe(Action callback);
subscribe : (() -> ()) -> ()

It does give some immediate clues as to where side-effects are in a type signature thought.

Types inside implementations

We’ve looked at different forms of type signatures, but this style also tends to work its way into method definitions, again using the form name : type.

Haskell tends to split the type signature from definition. F# specifies the arguments as argName : argType, and then gives the type of the resulting value (in this case List<'T>. Generic type parameters are indicated with a ' prefix. Swift uses a similar style, but an arrow is used for the return type. Swift needs explicit declaration of generic type parameters.

In both the Haskell and F# cases the type information can actually be omitted – the type system will infer the correct type signature.

Conclusion

This has been a quick tour through the things that first tripped me up when reading type signatures from non-C-style languages.

The main habit I needed to break was expecting to see a type then a name. Instead, names are first, then their type shown. So method types change like this:

Note that void in C-style languages is different to the terms “unit” and “Void” in non-C-style languages. In C-style languages void means “has no return value”, where a return type of () means “returns the value ()”. In contrast, the type Void is one with no legal instance. We can never return a value of type Void, so my understanding is a function a -> Void can never return.↩

Comments

All code and advice is provided without warranty -- use at your own risk! This is just a blog! Don't take it too seriously!
Despite not being too serious, this blog has a Privacy Policy, because it uses Google Analytics to see if anyone drops by.