What I Love about F#'s Type System

Now I know my A -> B 's, see? Next time won't you sing with me? Too much?

What I Love about F#'s Type System

'a -> 'b

It is such a beautiful thing.

'a -> 'b, where 'a and 'b are generic types. That function signature represents every possible function in F#.

On it's face it seems small and insignificant. But often when you have something simple and small you can use it as foundation for much greater things.

Take a "decorator" function that logs the input and output of calling a function passed as an argument.

let callWithLogging (fn : 'a -> 'b) (x : 'a) : 'b =
  log x
  let result = fn x
  log result
  result

callWithLogging takes a function that you want to log the input and output of, and returns a function that takes the same input, 'a, as the wrapped function.

Because of F#'s type system, callWithLogging can wrap any function in F# and log its input and output. The same cannot be said for almost any other language I've seen (excluding those sharing F#'s ML heritage).

The Gauntlet

Almost every programming language I've encountered struggles to play nicely with functions at some level. The problems I run into most often are the following:

Function Arity

The first problem we run into in many languages is functions that take more than one argument.

Lets use C# as an example. Here is callWithLogging translated to C#:

public B CallWithLogging<A, B>(Func<A, B> fn) => (A x) => {
	log x;
    var result = fn(x);
    log result;
    return result;
}

What happens when we want to decorate an "add" function? Something like

public int Add(int x, int y) => x + y;

Our code won't compile because Add is a Func<int, int, int>, which won't work with Func<A, B> because Func<A, B> only expects one argument and Add expects two.

We could change our C# Add function to make it work:

public int Add((int, int) xy) => xy.Item1 + y.Item2;

This version will work with CallWithLogging because instead of Func<int, int, int>, now we have a Func<(int, int), int>. Since we've modified Add to only expect one argument, Func<A, B> is happy. Our A is a (int, int) tuple and our B is an int.

Why doesn't F# have this problem?

Because every F# function only takes one argument.

Lets take a look. Here's the add function in F#:

let add (x : int) (y : int) : int = x + y;

It looks like this function takes two arguments, but it doesn't. adds signature is int -> int -> int, and anything after a -> in F# is a return type. It might be easier to see if we wrap the return type in parenthesis, int -> (int -> int). It's a function that returns a function, and the first function just takes one int.

If we wanted to pass both ints at the same time, we could write:

let add (xy : x * y) : int = (fst xy) + (snd xy)

Now we have a int * int -> int. Another single argument function. For anyone unfamiliar, the int * int syntax is how you represent a tuple in F#. In C#, we use (int, int). In F#, int * int.

Since all functions in F# take just one argument, 'a -> 'b can represent every function.

Arity is not an issue in F#.

Void Functions

If you're not familiar with F#, you might be wondering about functions that use void. In C#, void is not a type, so if you want to use Func<A, B> to represent a function that either has no arguments, or returns void, you can't.

In C#, if you want a function that has no arguments and returns a value, you have to use the signature Func<A>. If you want a function that returns void, you have to use an Action instead of a Func.

F# doesn't have this problem because void doesn't exist. F# uses a type called unit to represent "nothing". Since unit is an actual type, it can take the place of 'a and/or 'b in 'a -> 'b. In F#, when you pass unit to a function as an argument, it looks like this: (). Very similar to calling a C# method with no arguments. For example, if we had a random number generator called rand, with the signature unit -> int, we'd call it like this: rand ().

Similarly, a function with a signature like string -> unit, a function that would "return" void in many languages, works just as well. We'd call the function with a string, and we'd get () back from it.

void is not an issue in F#

F# may not have type-classes. It may not have higher-kinded types. But there's something to be said for consistency. You can build on top of consistency. It is a beautiful thing. And F#'s type system is nothing if not consistent