DEV Community

loading...
Cover image for Haskell - The Most Gentle Introduction Ever

Haskell - The Most Gentle Introduction Ever

Mateusz Podlasin
I write in-depth articles about JavaScript, React and functional programming - https://mpodlasin.com
Originally published at mpodlasin.com ・21 min read

Who Is This Article For?

This article is the first in (hopefully) a series on functional programming in Haskell.

It doesn't assume any previous knowledge of Haskell or even functional programming for that matter.

It does however assume that you can already program in some programming language.

If you feel fairly comfortable in a language like JavaScript, Python, Java, C/C++, or anything similar, you are more than capable of going through this tutorial. You can rest assured that everything will be explained slowly and carefully.

The main point of this series will be to highlight the differences between Haskell and those "typical" languages that I've mentioned. So the less you know about Haskell and/or functional programming, the more illuminating and mindbending those articles will be for you.

I will also be showing you how learning Haskell can benefit you in writing better code even when using other, more mainstream languages. If you feel stuck when it comes to your programming skills and if you feel like you haven't been stretching your coding muscles lately - Haskell will be perfect for you!

Whether you are a senior coding veteran or a junior dev that barely started your career, Haskell will push you to be an all-around better programmer. If you need more convincing, in the past I've written about why it is beneficial to learn Haskell, even if you don't plan to code in it professionally.

Are you ready? Let's go then!

Installing Haskellers Toolbelt

If you are on this journey with me, we'll begin by installing some software needed to run Haskell.

But if you are still unsure/unconvinced, you don't have to do even that. I will be keeping all the examples as simple as possible, so it will be enough to just use a REPL like this one. The only thing you need to do there is to empty the file in the REPL - we will start from scratch - and run ghci command in the terminal. You are ready now, so if you don't want to install Haskell tools on your computer, you can safely omit the rest of this section.

Still here? All in on learning Haskell? Awesome!

The download section on haskell.org can be a bit confusing, so I would recommend you to go straight to ghcup page. You just have to copy the script from the website, paste it to your terminal and run it.

ghcup is a "Haskell toolchain installer". This means that it allows you to easily install and manage various tools related to Haskell.

The installation process guides you by hand - you will just have to answer a few questions.

The installer will ask you about installing secondary packages - stack and hls.

hls is "Haskell Language Server". It allows IDE plugins and extensions to work seamlessly with Haskell. So it's worth having it since the very beginning.

stack is a build tool that allows you to create isolated Haskell projects easily. This would be massive overkill for simple, one-file scripts that we will be writing at the beginning. However, once we move on to writing more complex programs, Stack will be extremely helpful. Whether you prefer to install it now or later - it's up to you.

After running the script, you can test if everything went well by running the ghci command in your terminal.

We've mentioned quite a few tools so far, but ghci is the one that we will actually use in this article. It is an interactive environment for running Haskell code. ghc (no "i" at the end!) stands for Glasgow Haskell Compiler. ghc is in fact its own command, that will allow us to compile code into binary executables.

However, if we just want to play around with code and test some stuff, an interactive ghc - called ghci - is perfect for that, because it allows us to run Haskell code without the compilation step. On top of that, it has some handy commands which we will use today.

If you type in ghci in the terminal and get an error, it likely means that your terminal doesn't know how to find the ghci binary. You might have to close and reopen the terminal window after finishing the installation.

If it's still not working, then - depending on the environment you are using - you will have to edit your .bashrc or .zshrc file.

Luckily ghcup is very tidy - it just installs everything in a single directory, in my case /Users/mateusz.podlasin/.ghcup. In that directory, there is a bin folder. You need to point the terminal you are using to that folder.

So, in my case, I had to add the following line to my .zshrc file:

export PATH="/Users/mateusz.podlasin/.ghcup/bin:$PATH"
Enter fullscreen mode Exit fullscreen mode

After closing the terminal and opening it again, running ghci should now result in the following output:

GHCi, version 8.10.5: https://www.haskell.org/ghc/ :? for help
Prelude>
Enter fullscreen mode Exit fullscreen mode

If you see this, you are ready to begin our Haskell adventure.

Playing With Booleans

A boolean (in Haskell named Bool) is one of the simplest and most familiar types that a programmer encounters regularly. No matter what programming language you've used previously, you likely know booleans very well.

That's why they will be perfect for learning the basics of Haskell.

In Haskell boolean values are written starting with a big letter, so we have True and False.

In ghci you can type in:

:t True
Enter fullscreen mode Exit fullscreen mode

:t is a ghci command for checking the type of a value. Note that this command is not a part of Haskell itself, just a ghci functionality.

After clicking enter, as a result, you will see:

True :: Bool
Enter fullscreen mode Exit fullscreen mode

You can read :: as "has type". So this line says that the value True has type Bool.

Just to make sure we understand that correctly, let's check the type of value False:

:t False
Enter fullscreen mode Exit fullscreen mode

We see:

False :: Bool
Enter fullscreen mode Exit fullscreen mode

As you probably anticipated, the value False has type Bool as well. So True and False are of the same type. And in Haskell - quite reasonably - True and False are the only values of the type Bool.

Contrary to :t, the <value> :: <type> syntax is a part of Haskell language. And this should already hint at something to you. If a language has a dedicated syntax to express the sentence "<value> has type <type>", it means that this language probably treats types fairly seriously.

As we will soon see, types are at the very heart of programming in Haskell. As a matter of fact, sometimes when coding in Haskell you will be thinking about types more than about actual values!

And while we are here, note that the name of the type - Bool - is also written starting with a big letter in Haskell, just like the names of values. This will be important later.

Let's now start writing some actual code. We will still use ghci to run it, but we need to write it in an actual file.

Create a file called lesson_1.hs (in REPL I linked, the file is already created for you and is called main.hs). Note the .hs suffix, which represents Haskell source code files.

You can create that file anywhere you want. You can also edit it with any text editor you desire.

I would recommend using Visual Studio Code. After installing VS Code, immediately go to the "Extensions" tab (on the left) and install the "Haskell" extension. It will give you many handy features. It will for example start displaying the types of values you hover over. If you have trouble with the extension "not detecting ghc", remember to edit the PATH in the .bashrc/.zshrc file, just like I described in the setup section. After that restart the editor.

After you've created the file, make sure to be - using the terminal - in the same directory where the file is located. To do that, you can leave ghci by running :q. Then switch directories to a proper one and run the ghci command once again.

In ghci try loading the file, by running:

:l lesson_1.hs
Enter fullscreen mode Exit fullscreen mode

Even though your file doesn't have any code in it just yet, you should see a success message like this:

[1 of 1] Compiling Main ( lesson_1.hs, interpreted )
Ok, one module loaded.
Enter fullscreen mode Exit fullscreen mode

If you don't see that, you've likely run ghci in a different directory than your file is located.

But if you see the message, you are ready to begin coding!

So we have types (Bool) and values (True and False). What can we do with them? The most basic thing, known from other languages, would be to assign a value to a variable.

Write the following line in lesson_1.hs:

x = True
Enter fullscreen mode Exit fullscreen mode

Now load it again in ghci with the same command as before - :l lesson_1.hs (remember to save the file beforehand).

In ghci you can now type:

x
Enter fullscreen mode Exit fullscreen mode

As a response you will see:

True
Enter fullscreen mode Exit fullscreen mode

This shows that, indeed, we assigned the value True to the variable x.

Now let's do something that might surprise you and that will show you just like radically different Haskell is from "regular" languages.

In lesson_1.hs file let's write:

x = True
x = False
Enter fullscreen mode Exit fullscreen mode

Load it again in ghci. You will see the following output:

[1 of 1] Compiling Main ( lesson_1.hs, interpreted )

test.hs:2:1: error:
 Multiple declarations of ‘x’
 Declared at: lesson_1.hs:1:1
 lesson_2.hs:2:1
 |
2 | x = False
 | ^
Enter fullscreen mode Exit fullscreen mode

The most important part of that error message says that there are Multiple declarations of ‘x’.

It turns out that in Haskell, once you've assigned a value to a variable, you can't overwrite it or change it in any way. Ever.

If it's a Bool, you can't change it from True to False. If it's a number (which we will cover in future articles), you can't change its value. For example, you can't even increase the value of a numeric variable by one!

This sounds incredibly radical to someone used to traditional, imperative programming. Idioms like:

i++;
Enter fullscreen mode Exit fullscreen mode

or

someBoolean = !someBoolean;
Enter fullscreen mode Exit fullscreen mode

etc. are so prevalent in those languages that thought of only assigning a value to a variable once sounds... well... frankly just crazy!

Is it even possible to write actual, real-world programs with a language like this?

The answer is absolutely, and we will see that quite soon. But for now, you just have to accept that - once assigned a value - you can't ever change that variable anymore.

You've likely heard about immutability at this point. I already wrote about immutability in JavaScript for example.

That's the thing though. In those other, imperative languages immutability has to be introduced via some library or specific programming approach.

It's exactly the reverse in Haskell. Here immutability is the default and you have to use libraries or certain methods to achieve mutable variables/state.

That might sound cumbersome, but it's not an accident that the principle of immutability became so popular even in the mainstream, "mutable by default" languages. It really makes your code less buggy, safer, and more predictable.

Ok, enough of talky talk. So we know that once created, we can't alter that variable. But it is by no means useless. We can now call some functions using it.

Remove the x = False line from your file, so that loading the file in ghci works again and x has the value True.

Then run the following in ghci:

not x
Enter fullscreen mode Exit fullscreen mode

As an answer you will see:

False
Enter fullscreen mode Exit fullscreen mode

Now, if you've coded in Python or a language with similar syntax, you might think that not is some special, reserved keyword for negating booleans.

No. In Haskell, not is just a regular function.

In a typical language, a function call would look something like that:

not(x)
Enter fullscreen mode Exit fullscreen mode

But this is not the case in Haskell. In Haskell, you write the function name, and then - instead of parenthesis - you provide arguments by separating them with a single space.

So to call a function f on a variable x you would write:

f x
Enter fullscreen mode Exit fullscreen mode

If f accepted two parameters, instead of typical f(x, y) you would write:

f x y
Enter fullscreen mode Exit fullscreen mode

If f accepted three parameters, you would write:

f x y z
Enter fullscreen mode Exit fullscreen mode

And so on. You get the idea.

Let's go back to our not function. We called it on a variable, but nothing is preventing us from calling it on values directly.

In ghci type:

not True
Enter fullscreen mode Exit fullscreen mode

and you will see:

False
Enter fullscreen mode Exit fullscreen mode

Typing:

not False
Enter fullscreen mode Exit fullscreen mode

results in the response:

True
Enter fullscreen mode Exit fullscreen mode

Now, just as values, functions have types as well. We can investigate the type of not function in the same way we investigated the types of True and False - using the :t command in ghci.

Type:

:t not
Enter fullscreen mode Exit fullscreen mode

and you will see the following answer:

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

We again see the :: symbol, which means "has type". We see the Bool type mentioned twice.

The only new symbol here is ->. As you probably expect, <something> -> <something else> reads as "function from <something> to <something else>".

So the output that we got from ghci can be read as "not has the type of function from Bool to Bool". Or, more naturally, "not is a function from Bool to Bool".

This shouldn't be surprising. When we call not on a Bool value, we expect to see the Bool value as a result - the "opposite" of what we've passed. If we called not True and got 15 as an answer, we would be extremely confused, wouldn't we?

Writing Functions

At this point, I would like to prove to you that there is nothing magical about not. If it's just a regular function, you should be able to write it by yourself, right?

Yup, and that's exactly what we will do right now. We will write our first Haskell function!

We will do that in our lesson_1.hs file.

Under the x definition, write the following line:

myNot x = if x then False else True
Enter fullscreen mode Exit fullscreen mode

Let's break down what is happening here a little bit.

First, we have something that looks exactly like a call of a function - beginning with the name (myNot), and later the parameters of the function, separated by spaces. In this particular case, we have only one parameter, which we named x.

Next, we have an assignment (= character), after which we write the actual function - the part which we call "function body". In this case, the function body is just a simple if then else expression. Let's break it down.

Right after the if keyword we have to provide a condition. Our condition is really x == True. You probably recognize == from other languages. In Haskell it means the same thing - it's an equality operator.

But x == True is equivalent to simply writing x. After all, if x == True evaluates to True, this means that x itself has the value of True. So we can just write x as our condition, for brevity.

If the condition in if then else (our x) evaluates to True, the function will return the value after the then keyword. If it evaluates to False, the function will return the value after the else keyword. Quite simple.

Note that in Haskell if then else is an expression. This means that in the end, it evaluates to a value - one of the two provided after "then" and "else" keywords. In JavaScript for example this means that it has more common with a ternary operator, rather than regular if/else statement.

The following code in JavaScript:

const someVariable = condition ? firstValue : secondValue;
Enter fullscreen mode Exit fullscreen mode

would be therefore equivalent to the following in Haskell:

someVariable = if condition then firstValue else secondValue
Enter fullscreen mode Exit fullscreen mode

Let's return to the definition of our function:

myNot x = if x then False else True
Enter fullscreen mode Exit fullscreen mode

I will admit that I've used x as a parameter name here just to confuse you a little bit. For a second you might think that there is a naming conflict between x that we defined earlier and the x from the function.

You can however convince yourself that that's not true, by loading the file again in ghci. It loads properly, without any errors. On top of that, our newly defined function myNot actually works.

Calling in ghci:

myNot True
Enter fullscreen mode Exit fullscreen mode

returns

False
Enter fullscreen mode Exit fullscreen mode

and vice versa.

We can even use the function on our x variable defined above it.

Running:

myNot x
Enter fullscreen mode Exit fullscreen mode

results in:

False
Enter fullscreen mode Exit fullscreen mode

That is correct because in the file we assigned x to True.

So there is no naming conflict, but it is true that we are "shadowing" the x variable. If we now wanted to use it in the function body, we couldn't, because we decided to give the same name to the function parameter.

So, just for clarity, let's change the name of the function parameter to b (as in "boolean").

The whole file looks now like this:

x = True

myNot b = if b then False else True
Enter fullscreen mode Exit fullscreen mode

This will help us not get confused.

Perhaps the function definition that we came up with is not at all what you expected. We moaned for so long about the importance of types, but now we've written something that borderline looks like untyped Python. What's going on? Where are those scary types?

It turns out that Haskell's type system is so powerful, that most of the time it can infer what should be the type of a function - or a value - you've written. Save the file, load it in ghci and write:

t: myNot
Enter fullscreen mode Exit fullscreen mode

You will see:

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

So indeed your custom myNot function has the same type as the original not. But how Haskell came to that conclusion?

It's quite straightforward.

Since you wrote if x then ..., using x as a condition, Haskell knew that parameter x had to be a Bool. That's because Haskell is (again!) quite strict here and only the value of type Bool can be used as a condition in the if then else construct. (Note that we've written "as a condition" here! It's completely fine to provide values of other types after then and else keywords - we will see that in future articles.)

And at the same time, you wrote then False else True. Here, in both cases ("then" case and "else" case) you are returning a Bool. Therefore the output of your function has type Bool as well.

Those two facts combined bring us to a conclusion that the type of myNot has to be Bool -> Bool.

Haskell inference is extremely good and the compiler may only have problems if you write something that is inherently vague.

And yet it is still recommended to put the type signature of the function in the code. You will see that in the vast majority of Haskell codebases the types are always written explicitly.

You can do it by writing the type of the function above its definition:

myNot :: Bool -> Bool
myNot x = if x then False else True 
Enter fullscreen mode Exit fullscreen mode

Writing down the type in the code has some major advantages.

First of all, it increases the readability of the code. Types are incredibly useful as documentation of what your function does. In Haskell, you will often be able to find out what a given function does simply by looking at its name and type signature. No need to read the implementation!

Function called not that has type Bool -> Bool? It surely must be a function negating the booleans!

Second of all, it's valuable to write the type of the function before writing the function definition itself. If you do that, Haskell's type system will "guide you" and help validate that your code works as expected.

After all, inferred type of a function may differ from what you have intended.

Writing the type beforehand is almost like sketching or designing a function, before actually writing it. Personally, I find that writing down the type first often gives me a better idea of how to implement the function.

This is an example of a skill that Haskell teaches you, that you can easily transfer to other languages. Even when I'm writing untyped JavaScript, I still always start by thinking about what kind of type signature my function will have. This helps me to write code faster and make fewer bugs, even though I have to be my own type-checker in that case.

So we have successfully replicated the not function - myNot has the same type and behaves in the same way. Running myNot True evaluates to False, running myNot False evaluates to True.

But let's stay on this topic a bit longer and try to write the same function in a completely different manner.

Let's write:

myNot :: Bool -> Bool
myNot True = False
myNot False = True
Enter fullscreen mode Exit fullscreen mode

You can check for yourself that this loads properly in the ghci. You can also test that myNot still behaves the same as before.

What is happening here?

We used what is known as pattern matching.

Instead of declaring the parameter of the function as a variable named b, we can avoid naming it entirely and simply substitute it with a value that will be provided to the function once it's called.

When we make a call myNot True, Haskell looks for a definition that "fits" such a call. In this case, it's the first line (not counting the type signature). If we make a call myNot False, then it's the second line that matches that call. Hence the name "pattern matching".

Hopefully, it's clear that if you call myNot with a variable, not an actual value, pattern matching will still work just fine. In that case, Haskell simply evaluates the value of the variable and performs pattern matching then.

So with this new definition, calling:

myNot x
Enter fullscreen mode Exit fullscreen mode

Still properly returns:

False
Enter fullscreen mode Exit fullscreen mode

So we have two versions of the same function:

myNot :: Bool -> Bool
myNot b = if b then False else True
Enter fullscreen mode Exit fullscreen mode
myNot :: Bool -> Bool
myNot True = False
myNot False = True
Enter fullscreen mode Exit fullscreen mode

For all intents and purposes, these two functions behave in the same way (and have the same type). Which one you prefer depends entirely on you.

Do you want to see yet another way to write the same function? Here you go:

myNot :: Bool -> Bool
myNot b
 | b = False
 | otherwise = True
Enter fullscreen mode Exit fullscreen mode

Arguably this way of writing the function looks the most exotic.

It uses what is known as "guards". A guard is basically a condition, which is placed between | and = characters. When the condition is satisfied, a function body that is defined after = gets executed.

In our case, the first guard is b == True. But - just as before - we can write the same condition as simply b. If b is True, this condition is satisfied and the function body that returns False will be executed.

Otherwise (so when b is not True), the function body that returns True will be executed.

Interestingly, there is nothing magical about the keyword otherwise. (Are you noticing a pattern here, where some "feature" of the language is not really a feature, but simply something coded in that language?)

You can convince yourself that this is true by writing otherwise in the ghci console. As a response you will see:

True
Enter fullscreen mode Exit fullscreen mode

Yup. otherwise is nothing more than a regular variable, holding the value True! Just as our x is!

So, without the otherwise our "guarded" version of the function would look like this:

myNot :: Bool -> Bool
myNot b
 | b = False
 | True = True
Enter fullscreen mode Exit fullscreen mode

Just as in if then else, a condition has to be a Bool value. If the last condition is set to True, like it's the case here, it will always hold and therefore it will act as a catchall case if the guards above it fail. Calling it otherwise is just done to make this a bit more readable.

One more thing to mention here is that we used indentation. You see that lines starting with | characters are moved a bit to the right. If we didn't do it and wrote the code like this:

myNot :: Bool -> Bool
myNot b
| b = False
| True = True
Enter fullscreen mode Exit fullscreen mode

we would get an error while loading that file in ghci:

test.hs:3:1: error: parse error on input ‘|’
 |
3 | | b = False
 | ^
Enter fullscreen mode Exit fullscreen mode

Indenting code makes it more readable for humans and - as you can see - also helps the compiler understand that a given line is still a part of the definition that began in the previous line.

How much you indent the code is not important to the compiler - it has to be at least one space. But adding a few more spaces is better to keep the code nicely formatted for humans.

So we have written myNot in 3 different ways. Which one you prefer depends on you. Nevertheless, it's extremely valuable to get to know all 3 constructs that we've used - if then else, pattern matching and guards - because they appear almost all the time in Haskell code.

We used them in separation here, but in the future, you will see that you can mix those constructs in various ways, especially when writing more complex code.

Creating Types and Values

So far we have written a custom not function for the Bool type. But what if we could recreate the Bool type itself, as well as its values? Is it even possible?

Indeed, it's possible and even simple. Let's create a MyBool type. It will have two values - MyTrue and MyFalse. It might seem that we are getting a bit possessive here (wink, wink), but that's only to avoid conflicts with already existing names.

At the top of our lesson_1.hs file let's write:

data MyBool = MyTrue | MyFalse
Enter fullscreen mode Exit fullscreen mode

Easy right?

After you load that file in the ghci, you can run :t MyTrue to doublecheck that MyTrue has type MyBool, mirroring how True has type Bool:

MyTrue :: MyBool
Enter fullscreen mode Exit fullscreen mode

The same is true for MyFalse:

MyFalse :: MyBool
Enter fullscreen mode Exit fullscreen mode

Unfortunately, I lied to you a bit. If you type this in ghci:

MyTrue
Enter fullscreen mode Exit fullscreen mode

You will see a mysterious message:

<interactive>:68:1: error:
 • No instance for (Show MyBool) arising from a use of ‘print’
 • In a stmt of an interactive GHCi command: print it
Enter fullscreen mode Exit fullscreen mode

It happens because ghci tries to simply print the value, but... It doesn't know how!

This is part of the language that will cover in future articles. Luckily we don't have to worry about it for now. Haskell can create sane default for printing, you just have to command it to do that, by adding the following line to type definition:

data MyBool = MyTrue | MyFalse
 deriving (Show)
Enter fullscreen mode Exit fullscreen mode

If you now call ghci:

MyTrue
Enter fullscreen mode Exit fullscreen mode

It will simply print back:

MyTrue
Enter fullscreen mode Exit fullscreen mode

As I said - a sane default. In the future, we will learn how to customize that printing capability, but for now, it's perfectly fine.

You can now write functions that operate on this brand new type, just as we wrote a function for the built-in Bool type.

So if we wanted myNot to work on that custom MyBool type, we would do it like that:

myNot :: MyBool -> MyBool
myNot MyTrue = MyFalse
myNot MyFalse = MyTrue
Enter fullscreen mode Exit fullscreen mode

We simply replaced all occurrences of Bool with MyBool, all occurrences of True with MyTrue, and all occurrences of False with MyFalse.

We now have both a built-in version (Bool) and a custom version (MyBool) of a boolean type. Wouldn't it be convenient to write functions to switch between them?

Let's do it!

First let's write a boolToMyBool function. It will take a regular, Haskell Bool and transform it to our custom MyBool:

boolToMyBool :: Bool -> MyBool
boolToMyBool b = if b then MyTrue else MyFalse
Enter fullscreen mode Exit fullscreen mode

And let's write a function that will do the reverse:

myBoolToBool :: MyBool -> Bool
myBoolToBool MyTrue = True
myBoolToBool MyFalse = False
Enter fullscreen mode Exit fullscreen mode

We used two different ways to write a function on purpose here. Note that we wouldn't be able to use if then else with MyBool type as a conditional, because it works only on the built-in Bool type.

But on the other hand, we can use pattern matching in both, because pattern matching works with custom-defined values without any problems.

Great. We can now transform back and forth between built-in and custom types. Let's try that.

Load the file in ghci with both definitions and run:

boolToMyBool True
Enter fullscreen mode Exit fullscreen mode

You will see MyTrue as a response.

Now let's try to transform the same value back. Let's write:

myBoolToBool (boolToMyBool True)
Enter fullscreen mode Exit fullscreen mode

You will see True as the response.

What happened here is that we converted True to MyTrue using the boolToMyBool function (inside the parenthesis), and then we took that result and converted it back to True using the myBoolToBool function, all in a single expression.

Is it a bit nonsensical example? Perhaps, but it shows us how we can chain multiple function calls.

In fact, the brackets around the first function call are important.

Try to run the following in ghci:

myBoolToBool boolToMyBool True
Enter fullscreen mode Exit fullscreen mode

The compiler will show a fairly elaborate error message, but if you read it carefully, you will find the following sentence:

The function ‘myBoolToBool’ is applied to two arguments,
 but its type ‘MyBool -> Bool’ has only one
Enter fullscreen mode Exit fullscreen mode

Ha, so we found what is the problem. Indeed, our code looks as if we are trying to apply the myBoolToBool function to two parameters - boolToMyBool and True. Remember how f x y was representing calling a function f on two parameters? That's exactly what we are doing here! In that case, f is myBoolToBool, x is boolToMyBool and y is True.

We have to show the compiler that we want to apply the myBoolToBool function to a single parameter. And that parameter is a result of calling boolToMyBool on True. So we wrap that call in parenthesis to make that clearer (both to the compiler and us):

myBoolToBool (boolToMyBool True)
Enter fullscreen mode Exit fullscreen mode

Can we use those conversion functions for something more practical than needlessly translating booleans back and forth? Absolutely - we can use them to rewrite our myNot implementation once again.

We can use the fact that the Bool type has already the not function defined. It works exactly how we want, it just operates on different types. So let's take a MyBool value, convert it to Bool, use not on it, and then convert it back to MyBool:

myNot :: MyBool -> MyBool
myNot mb = boolToMyBool (not (myBoolToBool mb))
Enter fullscreen mode Exit fullscreen mode

Let's review step by step how that function works.

Since it's a function of type MyBool -> MyBool, its argument is a MyBool - that's why we called it mb. We pass that variable to a function - myBoolToBool mb - and as a result, we are getting something of type Bool. Then we are applying not on it, by writing not (myBoolToBool x). Note that at this step the type doesn't change - we are feeding not a Bool and getting a Bool again, since not is of type Bool -> Bool. At the end we convert back to MyBool, by writing boolToMyBool (not (myBoolToBool x)) - which is the return value of our function.

This could be again summarised as follows:

mb :: MyBool
Enter fullscreen mode Exit fullscreen mode
(myBoolToBool mb) :: Bool
Enter fullscreen mode Exit fullscreen mode
(not (myBoolToBool mb)) :: Bool
Enter fullscreen mode Exit fullscreen mode
(boolToMyBool (not (myBoolToBool mb))) :: MyBool
Enter fullscreen mode Exit fullscreen mode

This technique of tracking which value has which type is extremely valuable, especially when just starting with Haskell. Whenever you feel lost about how a certain function works, I would encourage you to analyze all the types of all the values in that function. You will discover that when you know all the types well, it's much easier to understand how the code works and what it does.

This is the second example of a skill that Haskell teaches you, which is immediately transferable to programming in other languages. Even if your day-to-day language is loosely typed and doesn't have types in its actual syntax, you can still use this way of thinking to better understand functions while reading code.

In time you will discover that this allows you to understand code much faster, especially the code that you haven't written yourself.

Conclusion

So we played with Bools in Haskell, serving as our introductory lesson in that language.

We've learned the basics of Haskell's syntax, we've seen how to create and use functions, and even how to create some simple datatypes.

In future articles, we will elaborate on those ideas quite a bit, so if you are hooked on Haskell by now (as I hope you are!), I would recommend to follow me on Twitter. I don't run any kind of newsletter email or anything like that, so that's the best way to get notified when a new article drops.

If you have any questions or comments regarding the article, you can reach me there as well. I would like to keep improving this article so that it serves others as best as possible.

Thanks for reading and see you soon!

Discussion (0)