C and C-style languages like C++, Java, and C# tend to have method types written like this:
Other typed languages and programming papers use a notation more like this:
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.
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.
: 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.
-> arrow separates function input from function output. So
a -> b reads as “I take values of type
a and produce values of type
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.
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
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:
map example above a “more exact, less idiomatic translation” was shown:
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.
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.
Curried function signatures in C# get unpleasant fairly quickly:
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,
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:
This starts to look a bit funny when methods take other calls with no input and no direct output:
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.
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:
Similarly arguments go from
ArgType name to
name : ArgType.
Hope this helps!
Such as Haskell, F#, Swift, Scala, OCaml, ML, Idris, Elm, PureScript, and TypeScript.↩
voidin C-style languages is different to the terms “unit” and “Void” in non-C-style languages. In C-style languages
voidmeans “has no return value”, where a return type of
()means “returns the value ()”. In contrast, the type
Voidis one with no legal instance. We can never return a value of type
Void, so my understanding is a function
a -> Voidcan never return.↩