This content originally appeared on DEV Community and was authored by Mateusz Podlasin
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"
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>
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
: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
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
We see:
False :: Bool
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
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.
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
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
As a response you will see:
True
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
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
| ^
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++;
or
someBoolean = !someBoolean;
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
As an answer you will see:
False
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)
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
If f
accepted two parameters, instead of typical f(x, y)
you would write:
f x y
If f
accepted three parameters, you would write:
f x y z
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
and you will see:
False
Typing:
not False
results in the response:
True
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
and you will see the following answer:
not :: Bool -> Bool
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
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;
would be therefore equivalent to the following in Haskell:
someVariable = if condition then firstValue else secondValue
Let's return to the definition of our function:
myNot x = if x then False else True
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
returns
False
and vice versa.
We can even use the function on our x
variable defined above it.
Running:
myNot x
results in:
False
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
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
You will see:
myNot :: Bool -> Bool
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
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
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
Still properly returns:
False
So we have two versions of the same function:
myNot :: Bool -> Bool
myNot b = if b then False else True
myNot :: Bool -> Bool
myNot True = False
myNot False = True
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
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
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
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
we would get an error while loading that file in ghci
:
test.hs:3:1: error: parse error on input ‘|’
|
3 | | b = False
| ^
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
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
The same is true for MyFalse
:
MyFalse :: MyBool
Unfortunately, I lied to you a bit. If you type this in ghci
:
MyTrue
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
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)
If you now call ghci
:
MyTrue
It will simply print back:
MyTrue
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
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
And let's write a function that will do the reverse:
myBoolToBool :: MyBool -> Bool
myBoolToBool MyTrue = True
myBoolToBool MyFalse = False
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
You will see MyTrue
as a response.
Now let's try to transform the same value back. Let's write:
myBoolToBool (boolToMyBool True)
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
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
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)
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))
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
(myBoolToBool mb) :: Bool
(not (myBoolToBool mb)) :: Bool
(boolToMyBool (not (myBoolToBool mb))) :: MyBool
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!
This content originally appeared on DEV Community and was authored by Mateusz Podlasin
Mateusz Podlasin | Sciencx (2021-07-29T19:13:49+00:00) Haskell – The Most Gentle Introduction Ever. Retrieved from https://www.scien.cx/2021/07/29/haskell-the-most-gentle-introduction-ever/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.