Saturday 25 July 2015

Haskell - the power of type declarations

Haskell - a very interesting functional language, which continuously helps me become a better programmer and a more efficient thinker. One small example (exercise 1.24) I found in the book I'm currently reading (The Haskell Road to Logic, Maths and Programming) made me think: "wow, this is just great". This "wow" moment is something I would like to share with you today. I am going to show you that it's the type declaration and not the actual body of the function that you should consider your best friend while reasoning about the function's job. Haskell, as a strongly and statically typed language lets you do that. If you think it's crazy, please read on!

Let's look at a small program:

add2 :: Integer -> Integer
add2 x = (2 + x)

main = print $ add2 1 -- prints "3"
If you're a Haskell virgin, I hope you can still follow what's happening (if not, take a look at some other examples from e.g. Wikipedia to get a feel of the language). The add2's type declaration (first line) states:

I transform Integers into Integers.

Pretty simple. However, you might come across a slightly different interpretation:

I take one parameter of type Integer and return an Integer.

Which also works in our simple example. You might now ask: "ok, so now how do we write type declarations for functions which take more than one parameter?".
add x y = x + y
I remember asking this question myself and experimenting with constructs like:
add :: Integer Integer -> Integer
add x y = x + y
or
add :: Integer, Integer -> Integer
add x y = x + y
but none of them worked. At the time I just accepted that the compiler is smart enough to determine that the last "->" denotes the returned type and every other "->" simply chains parameters together. Weeks have passed and I started reading about lambdas, currying, partial application. I never looked back at those failed experiments from the beginning of my journey. Until now.

Let's return to our first function - add2. Exercise 1.24 (or its variant, adapted to fit my add2 example) makes you remove the parameter "x" and see what happens. Haskell allows for (and eagerly supports!) partial application of functions, so in our example, the additon is just partially applied (we only have one out of two parameters: 2). Not a big thing. Let's remove the "x" and look at the code now:

add2 :: Integer -> Integer
add2 = (2 +)

main = print $ add2 1 -- prints "3"
This is when I started connecting the dots. I expected the code to work, but I never thought of this:
  1. If my function (partially applying addition) works without the parameter, it really is nothing more than just an alias for that partial application of addition. This was a very powerful thought as it was always natural to me to associate the assignment operation with some sort of an order for the computer to "run the code on the right to give value to the code on the left". I started asking more questions:
    • If we don't need the "x" now, have we ever really needed it?
    • Is this going to change the way I think about code (not only the functional code)?
    • If so, how will all my programs look from now on?
    I am in the process of looking for answers to these questions and I find it very fresh and exciting.
  2. Let's look at the type declaration again - it hasn't changed! This made me think that my perception of type declarations was not very accurate. I realised that it serves as the promise of what the function is expected to achieve. The body obviously does the heavy lifting and fleshes the type declaration out, but it's the type declaration that helps understand the big picture. In the end, it does not really matter if your function takes one parameter and returns its processesed value or takes zero parameters and returns (or acts as!) a function that does it. What matters is the transformation defined in the type declaration. Implementation is just a set of various operations that should follow the principles outlined in the type declaration.

Learning Haskell (and functional programming in general) is a great experience which I can highly recommend to every software engineer. I plan to continue reading and understanding Haskell as it's a great source of inspiration and ideas. I've been writing some sandbox Haskell code in my free time, which you can find here and contribute if you please.

I hope this makes sense to you and if not (or if so!), please feel free to leave a comment in the section below.

2 comments:

  1. Did you think to ask the interpreter the type?

    λ> let add x y = x + y
    λ> :t add
    add :: Num a => a -> a -> a
    λ>

    For the readers (I assume you've figured it out), this is "add takes a number and returns a function that takes a number and returns a number". It's actually a nice usage, because you can now define your original add2 as:

    add2 :: Integer -> Integer
    add2 = add 2

    and it just works. And yes, I've made the type stricter than it was. So you can say "add 2 3.0" and it works, but "add2 3.0" is a type errorl

    ReplyDelete
    Replies
    1. Mike,

      Thanks for your comment.

      Interesting proposition. It's great that Haskell lets us do this sort of thing (narrowing the types down, to Integer in this case, as they are needed/used).

      Delete