DEV Community

Cover image for Haskell - The Most Gentle Introduction Ever (Part II)
Mateusz Podlasin
Mateusz Podlasin

Posted on • Originally published at mpodlasin.com

Haskell - The Most Gentle Introduction Ever (Part II)

This is the second article in my little series explaining the basics of Haskell.

If you haven't yet, I would recommend you to read the first article, before diving into this one.

So far we've learned the basics of defining functions and datatypes, as well as using them, all based on the Haskell Bool type.

In this article, we will deepen our understanding of functions in particular, while also learning a bit about built-in Haskell types representing numbers.

Type of Number 5

Before we begin this section, I must warn you. In the previous article, I purposefully used the Bool type, due to its simplicity.

You might think that types representing numbers would be equally simple in Haskell. However, that's not really the case.

Not only does Haskell have quite a lot of types representing numbers, but there is also a significant effort in the language to make those types as interoperable with each other as possible. Because of that, there is a certain amount of complexity, which can be confusing to beginners.

To see that, type in the following in the ghci:

:t 5
Enter fullscreen mode Exit fullscreen mode

You would probably expect to see something simple and concrete, like Number or Int. However, what we see is this:

5 :: Num p => p
Enter fullscreen mode Exit fullscreen mode

Quite confusing, isn't it?

For a brief second try to ignore that whole Num p => part. If it wasn't there, what we would see, would be just 5 :: p So what is written here is that the value 5 has type p.

This is the first time we see the name of the type being written using a small letter. That's important. Indeed, p is not a specific type. It is a type variable. This means that p can be potentially many different, concrete types.

It wouldn't however make sense for 5 to have - for example - Bool type. That's why Num p => part is also written in the type description. It basically says that this p has to be a numeric type.

So, overall, 5 has type p, as long as p is a numeric type. For example, writing 5 :: Bool would be forbidden, thanks to that restriction.

The exact mechanism at play here will not be discussed right now. We still have to cover a few basics before we can explain it fully and in detail. But perhaps you've heard it about it already - it's called typeclasses. We will learn about typeclasses very soon. After we do, this whole type description will be absolutely clear to you.

For now, however, we don't need to go into specifics. Throughout this article, we will use concrete, specific types, so that you don't get confused. I am just warning you about the existence of this mechanism so that you don't get unpleasantly surprised and discouraged when you investigate the types of functions or values on your own.

Which I encourage you to do! Half of reading Haskell is reading the types, and you should be getting used to that.

Functions and Operators

Let's begin by doing some simple operations on numbers, familiar from other programming languages.

We can, for example, add two numbers. Writing in ghci:

5 + 7
Enter fullscreen mode Exit fullscreen mode

results in a fairly reasonable answer:

12
Enter fullscreen mode Exit fullscreen mode

But to get a deeper insight into what is happening there, let's write a "wrapper" function for adding numbers.

We will call it add and we will use it like so:

add 5 7
Enter fullscreen mode Exit fullscreen mode

As a result, we should see the same answer as just a moment ago:

12
Enter fullscreen mode Exit fullscreen mode

We would like to, of course, begin with a type signature of that function. What could it potentially be?

add :: ???
Enter fullscreen mode Exit fullscreen mode

As I mentioned before, Haskell has many different types available for numerical values.

Let's say that we want to work only with integers for now. Even for integers, there are multiple types to choose from.

The two most basic ones are Integer and Int.

Int is a type that is "closer to the machine". It's a so-called fixed precision integer type. This means that - depending on the architecture of your computer - each Int will have a limited number of bits reserved for its value. Going out of those bounds can result in errors. This is a type very similar to C/C++ int type.

Integer on the other hand is an arbitrary precision integer type. This means that the values can potentially get bigger than those of Int. Haskell will just reserve more memory if that becomes necessary. So those integers are "arbitrarily" large, but, of course, only in principle - if you completely run out of computer memory, nothing can save you.

At the first glance, it would seem that Integer has some clear advantages. That being said, Int is still being widely used where memory efficiency is important or where we have high confidence that numbers will not become too large.

For now, we will use the Integer type. We can finally write the signature of our function:

add :: Integer -> Integer -> Integer
Enter fullscreen mode Exit fullscreen mode

What we have written here is probably a bit confusing at the first glance.

So far, we've only written declarations of functions that accept a single value and return a single value.

But when adding numbers, we have to accept two values as parameters (two numbers to add) and then return a single result (a sum of those numbers). So in our type signature, the first two Integer types represent parameters and the last Integer represents the return value:

add :: Integer {- 1st parameter -} -> Integer {- 2nd parameter -} -> Integer {- return value -}
Enter fullscreen mode Exit fullscreen mode

(By the way, you can see here what syntax we've used to add comments to our code.)

Let's now write the implementation:

add :: Integer -> Integer -> Integer
add x y = x + y
Enter fullscreen mode Exit fullscreen mode

Simple, right?

Create a file called lesson_02.hs and write down that definition. Next, load the file in ghci (by running :l lesson_02.hs) and type:

add 5 7
Enter fullscreen mode Exit fullscreen mode

As expected, you will see the reasonable response:

12
Enter fullscreen mode Exit fullscreen mode

I will now show you a neat trick. If your function accepts two arguments - like it is the case with add - you can use an "infix notation" to call a function. Write:

5 `add` 7
Enter fullscreen mode Exit fullscreen mode

You will again see 12 as a response.

Note that we didn't have to change the definition of add in any way to do that. We just used backticks and we were immediately able to use it in infix notation.

You can use this feature with literally any function that accepts two parameters - this is not a feature restricted only to functions operating on numbers.

At this point those two calls:

5 `add` 7
Enter fullscreen mode Exit fullscreen mode

and

5 + 7
Enter fullscreen mode Exit fullscreen mode

look eerily similar.

That's not an accident.

Indeed, you can do the reverse, and call the + operator as you would call a regular function - in front of the parameters. If it's an operator, you just have to wrap it in parentheses:

(+) 5 7
Enter fullscreen mode Exit fullscreen mode

Try it in ghci. This works and returns 12 again!

Perhaps you already know where am I going with this.

I am trying to show you that the built-in + operator is indeed just a regular function.

There are of course syntactical differences (like using backticks or parentheses for certain calls), but conceptually it is good to think of + as being no different than add. Both are functions that work on the Integer type - accept two Integer numbers and return an Integer number as a result.

Indeed, you can even investigate the type of an operator in ghci, just as you would investigate the type of add function.

Typing:

:t (+)
Enter fullscreen mode Exit fullscreen mode

results in:

(+) :: Num a => a -> a -> a
Enter fullscreen mode Exit fullscreen mode

This means that + has type a -> a -> a, where a has to be a numeric type. So it's a function that accepts two parameters of numeric type a and returns the result of the same type.

I hope that at this point it makes sense why it is beneficial for the + operator to have such an abstract definition. A clear benefit + has over our custom add function is that + works on any numeric type. No matter if it's Integer, Int, or any other type that somehow represents a number - + can be used on it. Meanwhile, our add function works only on Integer types. For example, if you try to call it on - very similar - Int numbers, the call will fail.

So you can see that complexity introduced in number types doesn't come out of nowhere. It keeps the code typesafe, while still allowing huge flexibility. Types might seem complex, but this makes writing actual implementations a breeze.

Partial Application

Let's go back to the type of add function, which is probably still friendlier to read at this point.

add :: Integer -> Integer -> Integer
Enter fullscreen mode Exit fullscreen mode

The way we have written the type definition here might be surprising to you. We see two -> arrows in the definition, almost suggesting that we are dealing with two functions.

And indeed we are!

To increase the readability even more, we can use the fact that -> is right-associative. This means that our type definition is equivalent to this:

add :: Integer -> (Integer -> Integer)
Enter fullscreen mode Exit fullscreen mode

Let's focus on the part that is outside of the parentheses first:

add :: Integer -> (...)
Enter fullscreen mode Exit fullscreen mode

This says that add is a function that accepts an Integer and returns something.

What is that something? To find out, we have to look inside the parentheses:

(Integer -> Integer)
Enter fullscreen mode Exit fullscreen mode

That's again a function! This one also accepts an Integer as an argument. And as a result, it returns another Integer.

So if we look at the type of add again:

add :: Integer -> Integer -> Integer
Enter fullscreen mode Exit fullscreen mode

we see that - in a certain sense - I was lying to you the whole time!

I was saying that this is how we describe a function that accepts two parameters. But that's false! There is no function that accepts two parameters here!

There is only a function that accepts a single parameter and then... returns another function!

And then that second function accepts yet another parameter and just then returns a result!

To state the same thing in terms of another language, here is how you would write a regular function that accepts two parameters in JavaScript:

function add(x, y) {
  return x + y;
}
Enter fullscreen mode Exit fullscreen mode

However what we are creating in Haskell is something closer to this JavaScript code:

function add(x) {
  return function(y) {
    return x + y;
  }
}
Enter fullscreen mode Exit fullscreen mode

Note how we have two functions here, each accepting only a single parameter.

In the code snippet above, function add accepts parameter x and the second, anonymous, function accepts the parameter y. There is no function here that accepts two parameters.

So, based on what we have said so far, in Haskell we should be able to call the add function with only one parameter and get a function, right?

Let's try that in ghci:

add 5
Enter fullscreen mode Exit fullscreen mode

Regrettably, we get an error message:

<interactive>:108:1: error:
 • No instance for (Show (Integer -> Integer))
 arising from a use of ‘print’
 (maybe you haven't applied a function to enough arguments?)
 • In a stmt of an interactive GHCi command: print it
Enter fullscreen mode Exit fullscreen mode

But that doesn't happen, because we did something wrong. The problem arises, because add 5 returns - as we stated - a function, and Haskell doesn't know how to print functions.

We can however check the type of the add 5 expression and this way convince ourselves that this indeed works:

:t add 5
Enter fullscreen mode Exit fullscreen mode

As a result, we see:

add 5 :: Integer -> Integer
Enter fullscreen mode Exit fullscreen mode

So it's a success! The expression add 5 has type Integer -> Integer, just as we wanted!

We can convince ourselves even more that that's the case, by calling that add 5 function on a number:

add 5 7
Enter fullscreen mode Exit fullscreen mode

Oh... Wait. We just discovered something!

It was probably confusing so far why Haskell has this strange way of calling functions on values, especially on multiple values. We were just separating them by space, like so:

f x y z
Enter fullscreen mode Exit fullscreen mode

Now it becomes clear, that Haskell simply deals with single-argument functions all the time, and calling a function on multiple values is simply an illusion!

Our call:

add 5 7
Enter fullscreen mode Exit fullscreen mode

is equivalent to:

(add 5) 7
Enter fullscreen mode Exit fullscreen mode

First, we apply add to 5 and as a result, we get a function of type Integer -> Integer, as we've just seen.

Then we apply that new function (add 5) on a value 7. As a result, we get an Integer - number 12.

Note that this means that in Haskell function call is left-associative:

(add 5) 7
Enter fullscreen mode Exit fullscreen mode

Contrasting with what we found out before - that type definition of function is right-associative:

add :: Integer -> (Integer -> Integer)
Enter fullscreen mode Exit fullscreen mode

It is of course done this way so that we get a sane default. Thanks to those properties, in the case of both defining and calling the add function, we can simply forget about parentheses:

add :: Integer -> Integer -> Integer
Enter fullscreen mode Exit fullscreen mode
add 5 7
Enter fullscreen mode Exit fullscreen mode

How else can we convince ourselves that the result of calling add 5 is an actual, working function? Well... let's give a name to that function and use it that way!

In your lesson_02.hs file add the following line:

addFive = add 5
Enter fullscreen mode Exit fullscreen mode

Load the file in ghci.

First let's investigate the type one more time, just to be sure what we are dealing with:

:t addFive
Enter fullscreen mode Exit fullscreen mode

This gives us:

addFive :: Integer -> Integer
Enter fullscreen mode Exit fullscreen mode

Exactly what we expected after applying the add function to one argument.

We could have also written down that type definition explicitly in our file:

addFive :: Integer -> Integer
addFive = add 5
Enter fullscreen mode Exit fullscreen mode

Do that to convince yourself that it all compiles when written this way.

Now you can use your, highly specific, addFive function to... add five to integers.

addFive 7
Enter fullscreen mode Exit fullscreen mode

This results in the expected:

12
Enter fullscreen mode Exit fullscreen mode

Now, I am sure this example with adding the number five seems a bit silly to you, and rightfully so.

But I hope that it shows you the power of partial application in Haskell, where you can easily use highly general functions, accepting a higher number of parameters, to create something more specific and fitting your particular needs.

For example, you can imagine a function that needs some kind of complex configuration in order to work - let's call it imaginaryFunction.

Let's assume that this configuration is some kind of data structure of type ComplexConfiguration. We can make that configuration the first argument of our function:

imaginaryFunction :: ComplexConfiguration -> OtherParameter -> Result
Enter fullscreen mode Exit fullscreen mode

Why do we want to pass it as a parameter instead of just having it "hardcoded" inside the function? Who knows, perhaps different versions of our app need different configurations. Or perhaps we just need to change its value in our unit test suite.

If we do that, then, in the actual app, we can simply apply imaginaryFunction to a specific ComplexConfiguration:

configuredImaginaryFunction = imaginaryFunction config
Enter fullscreen mode Exit fullscreen mode

After that, we can use the configuredImaginaryFunction directly, without the need to explicitly import config object, whenever we want to use imaginaryFunction in our code. Haskell just carries that config around for us!

Sweet!

More Numbers and Operations

This section will be far from a complete rundown, but I still want to quickly take you up to speed with basic operations on numbers in Haskell.

So far we've covered two numeric types - Integer and Int and we've shown that we can add them.

Just as in other languages, we can also subtract and multiply them in the usual way.

So:

5 - 2
Enter fullscreen mode Exit fullscreen mode

results in:

3
Enter fullscreen mode Exit fullscreen mode

while:

5 * 3
Enter fullscreen mode Exit fullscreen mode

results in:

15
Enter fullscreen mode Exit fullscreen mode

Other popular number types are Float and Double. Those are number types that can represent numbers beyond simple integers. The main difference between Float and Double is how big their storage capacity is. Most of the time you will likely use Double unless you have some specific reason to use Float.

Float and Double values can be added, subtracted, and multiplied just like Int and Integer values.

Let's see some examples, just to make ourselves comfortable with that:

1.1 - 0.1
Enter fullscreen mode Exit fullscreen mode

results in:

1.0
Enter fullscreen mode Exit fullscreen mode
1 + 0.1
Enter fullscreen mode Exit fullscreen mode

result in:

1.1
Enter fullscreen mode Exit fullscreen mode

Note that in Haskell's standard library there are much more numeric types available. Some of them are fairly common, others are fairly specific. There are also many more built-in functions.

This section was just meant to make you comfortable with making simple operations on basic number types. In the future we will come back to numbers for sure - there is a great deal of interesting type-level stuff at play here, and we will want to cover that for sure!

Identity

Let's now take a small break from numbers and go back to our beloved booleans.

Previously, we have written a not function, which was negating the booleans - converting True to False and False to True.

But what if we wanted to do... the opposite?

What if we wanted to create a function that... returns True when passed True and returns False when passed False?

This might sound a bit nonsensical. In a way, this function would do literally nothing. However, we will see that it will have a tremendous educational value for us, so let's curb our doubts and let's try to write it anyway.

First, let's start with the name and type signature as all Haskell programming should.

In mathematics, a function that takes a value and returns the exact same value is usually called an identity function. Since this one will operate on the Bool type, we will call it boolIdentity.

boolIdentity will receive a single argument of type Bool and return the same thing, so... Bool as well! Therefore its type signature is this:

boolIdentity :: Bool -> Bool
Enter fullscreen mode Exit fullscreen mode

Now let's get started with actual implementation. Your first instinct might be to write something like this:

boolIdentity True = True
boolIdentity False = False
Enter fullscreen mode Exit fullscreen mode

This will work perfectly fine and is a valid solution, but there is a way to write the same thing in a more terse way.

After all, we are returning the same value that we are receiving as a parameter, so we can simply write:

boolIdentity x = x
Enter fullscreen mode Exit fullscreen mode

In the end, our whole definition looks like that:

boolIdentity :: Bool -> Bool
boolIdentity x = x
Enter fullscreen mode Exit fullscreen mode

Write that down in your lesson_02.hs file and reload the file in ghci.

You can convince yourself that our function works, by running it:

boolIdentity True
Enter fullscreen mode Exit fullscreen mode

This call results in:

True
Enter fullscreen mode Exit fullscreen mode

And at the same time boolIdentity False returns False (hopefully not surprisingly).

Now let's create a similar function, but for numbers - let's say for Integer type. We want a function that will take an Integer value and return the same value. For example, if we call it with 5, we want to see 5 again.

Let's call it integerIdentity. Let's begin by writing the type signature:

integerIdentity :: Integer -> Integer
Enter fullscreen mode Exit fullscreen mode

This was simple.

Now let's think about the implementation. Well... we want to take the parameter passed to the function and... just return it!

So we get:

identityInteger x = x
Enter fullscreen mode Exit fullscreen mode

But... but this is exactly the same implementation as in the case of identity for Bool values!

Let's compare the two:

boolIdentity :: Bool -> Bool
boolIdentity x = x
Enter fullscreen mode Exit fullscreen mode
integerIdentity :: Integer -> Integer
integerIdentity x = x
Enter fullscreen mode Exit fullscreen mode

Everything looks the same. The only difference here is the types. In the first function, we operate on the Bool type. In the second we operate on the Integer type.

Now, if only there was a way to write that function only once. In the current state of things, we would have to write an identity function for each type in existence, which... sounds daunting, to say the least.

It luckily turns out that Haskell does have a mechanism to deal with that easily. Not only that - we've already encountered that mechanism!

Remember how the type of number 5 was Num p => p? The p in the type description was a type variable - basically a placeholder for actual, concrete types.

We've also seen that the + operator was quite general - it could be called on any numeric type. It also had a type variable in its type signature.

So the question is, can we use a type variable, to write the most generic version of the identity function possible? The answer is... absolutely!

Let's remove the two previous identity functions and replace them with only one:

identity :: a -> a
identity x = x
Enter fullscreen mode Exit fullscreen mode

Note how we used a as a type variable here. The 3 type definitions we've seen so far, share the same "shape". You can see that the type definitions of identity for Bool type and for Integer type both "fit" this new type definition if you imagine variable a being a placeholder for other types:

boolIdentity :: Bool -> Bool
Enter fullscreen mode Exit fullscreen mode
integerIdentity :: Integer -> Integer
Enter fullscreen mode Exit fullscreen mode
identity :: a -> a
Enter fullscreen mode Exit fullscreen mode

Let's run the code in ghci and convince ourselves that we can indeed use this new, generic identity on both Bool and Integer values:

identity True
Enter fullscreen mode Exit fullscreen mode

works and results in:

True
Enter fullscreen mode Exit fullscreen mode

And at the same time:

identity 5
Enter fullscreen mode Exit fullscreen mode

works as well and results in:

5
Enter fullscreen mode Exit fullscreen mode

At this point, it's important to emphasize a certain point. Given the implementation that we've used for the identity function:

identity x = x
Enter fullscreen mode Exit fullscreen mode

we could not give it, for example, the following type:

identity :: Integer -> Bool
Enter fullscreen mode Exit fullscreen mode

This type definition in itself is not absurd. You can easily imagine functions that accept integers and return true or false (for example based on some condition).

However, in this particular case, where we take argument x and immediately return it, without doing anything else, it's clearly impossible for x to "magically" change the type.

And this fact is reflected even in the most generic type definition of identity:

identity :: a -> a
Enter fullscreen mode Exit fullscreen mode

Note that this definition states that identity accepts a value of type a and returns a value of that same type - namely a again.

On the flip side, the following definition:

identity :: a -> b
Enter fullscreen mode Exit fullscreen mode

wouldn't be allowed. In fact, it won't compile, with an error, part of which says:

Couldn't match expected type ‘b’ with actual type ‘a’
Enter fullscreen mode Exit fullscreen mode

So the compiler literally says that in place of b there should be the type variable a present.

That's because - given the current implementation - it's impossible for the value named x to just change the type out of nowhere.

And indeed, when Haskell infers the type of untyped code, it goes for the most general interpretation possible.

You can convince yourself of that, by removing the type definition from lesson_02.hs file, and leaving only the implementation:

identity x = x
Enter fullscreen mode Exit fullscreen mode

And then compiling it in ghci and checking the type of identity by running:

:t identity
Enter fullscreen mode Exit fullscreen mode

As an answer you will see:

identity :: p -> p
Enter fullscreen mode Exit fullscreen mode

This is exactly the same type definition that we wrote by hand. It simply uses a different letter (p instead of a).

At the very end of this section, it would be good to mention, that you don't actually have to define the identity function by yourself. We only did it for educational purposes.

Prelude - the standard library for Haskell - has it always available for you under the shorter name id.

To convince yourself of that, write the following in ghci:

:t id
Enter fullscreen mode Exit fullscreen mode

As a response you will see, more than familiar now, type:

id :: a -> a
Enter fullscreen mode Exit fullscreen mode

Conclusion

In the second part of "The Most Gentle Introduction Ever" to Haskell, we used operators and functions on numbers and discovered that they are in fact almost the same. We've also seen that there are no functions of multiple variables in Haskell - they are just functions that return other functions.

And at the end, we expanded our understanding of what a type definition can be, by showing the simplest possible usage of type variables - using the identity function as an example.

All those things were meant to make you feel more comfortable with the concept of a function in Haskell. And in the future article, we will use a similar approach to enhance our understanding of algebraic data structures, especially custom ones.

So see you next time and thanks for reading!

Discussion (0)