Getting Started with Haskell
This is me talking about what I think will help people get started with Haskell. If you're interested in category theory and the more theoretical parts of Haskell, I definitely think you should read Category Theory for Programmers by Bartosz Milewski (this isn't a pre-requisite for this blog, just something that you might want to read later).
Installation
To get started with Haskell, you need two tools: cabal
and ghc
(the Glorious Glasgow Haskell Compiler). A very easy way to install ghc is using ghcup. Once that's done, cabal
can be installed using ghcup install-cabal
.
You're also going to need the language server, which helps a lot during development. If you're going to use VSCode, the installation will happen automatically. If not, check this out.
Note that at the time of writing, the language server supports GHC 8.10.15 (fully) and GHC 9.0.1 (core works, some plugins don't) and other older versions only. It's recommended you use one of these two. You can do this using ghcup install 8.10.15
and ghcup set 8.10.15
, for example.
To create a project, create an empty directory, cd
into it, and execute cabal init -n
.
To run the project, execute cabal new-run
.
Basics
Haskell is a purely-functional, lazy, statically-typed, garbage-collected, compiled programming language. It gives you a binary that you can execute without needing any kind of runtime installed on your machine, though Haskell binaries are at times quite large since they have all the dependencies and the Haskell runtime statically linked. It is, however, possible to build dynamically-linked binaries, but you then need to provide all the dependencies to the end-user anyway, so a fat binary is usually the way to go. Haskell is very performant and can achieve near-C speeds if written carefully.
Purely Functional
This means that all functions in Haskell take a value and return a value – nothing more. For example, consider the following code sample from Javascript:
Here, the function add
does something more than just calculating a value and returning it -- counter++
. This is what's called a side effect.
When a function relies on or modifies something outside of it's arguments, it's said to have side-effects.
This means that, naturally, all I/O is a side effect, since you're relying on or modifying the filesystem or a network instead of simply taking in values and returning a value, which can possibly impact the return value of the function between multiple runs.
Think of pure functions as mathematical functions that map values from one set to another; mapping is all they do. If you have a function f(x) = x * 2
, the result of the function will always be the same, no matter how many times you evaluate it, and there's nothing 'modifiable' in the sense of mathematical functions.
The benefit of having pure functions is that you don't have to think twice before calling a function. You know that calling a function will only 'transform' one value into another, and you don't have to worry about it internally changing some state or performing some actions that you didn't know or forgot about.
Lazy
Haskell does not evaluate expressions until it needs to. For example, if you have some code like
function multiply(x, y) {
const unnecessary = (x*y) * (x*y);
return x*y;
}
In a language that's not lazily-evaluated, the linter will give you some kind of error like unused variable "unnecessary"
, and if you don't remove this line, you'll incur some performance penalty because this value will always be computed, even though it's not used (Though compilers can be smart enough to remove such computations). This won't happen in Haskell because values are only constructed when they're needed.
Laziness allows you to create infinite lists, which can be helpful for example when you need the first 50 positive integers – you can just create an infinite list and take
the first 50 elements, and the rest will not be created simply because they're not needed.
Statically Typed
I'm sure you know what this means, but you probably haven't seen algebraic data types before, so have a look – it's pretty self explanatory.
Tree
here is a type that can either be a Node
or a Branch
– this is what an algebraic data type is. They are called so because they are types formed by algebraic operations. In our example, the type Tree
is formed by the operation '+' which denotes 'or' between the type constructors Node
and Branch
, and the type Branch
is formed by a '*' or 'combination' of Tree
and Tree
.
Note that Node
and Branch
here are data constructors. They take arguments and construct a value of type Tree
. Note that we ask for an Int
to construct a Node
. If we were to generalize this, we could do something like
data Tree a = Node a | Branch (Tree a) (Tree a)
Here, Tree
is a type constructor, you give it a type (not value!) argument and it constructs a type (not value!). Type constructors construct types, data constructors construct values. For example, Tree Int
will construct the type Node Int | Branch Tree Tree
.
The data
keyword is used to declare algebraic data types. The keyword type
can be used to create a type synonym this way:
type Money = Int;
The compiler won't warn you when you try to pass an Int
to a function that takes Money
. It's just a synonym and nothing more.
Algebraic Data Types (ADTs) are widely used in Haskell.
Garbage Collected
The concept of GC in Haskell is the same as in other languages. This link might be helpful if you're wondering why Haskell needs a Garbage Collector, and you can check this link out for more information about Haskell's memory management model.
Starting out with some code
Let's have a look at some basics. Here's a sample program:
- Haskell code is written inside modules. You can import other modules after the first line to use stuff defined inside them. In the end, all the modules that you used are compiled together, and the
main
function in theMain
module is run. - In most languages, you would call a function like
functionName(param1, param2)
; but in Haskell, you don't need parantheses. The same call would look likefunctionName param1 param2
. - The
()
here is an empty tuple. If you know a bit of Python, you already know what Tuples are. They're a kind of list that can have elements of different data types. Another example of a tuple is("Hi", 5, 7) :: (String, Int, Int)
. - The
$
you see in the second last line is just a neat little way to avoid brackets. Here, if we don't use the$
, it could be interpreted as
(putStrLn "Hello, ") <> name <> " of age " <> age <> "!"
But this isn't what we want, we want to combine all the strings and pass those as an argument. You could also do this like
putStrLn ("Hello, " <> name <> " of age " <> age <> "!")
- The
main :: IO ()
is the type signature for the functionmain
. For example,
sum :: Int -> Int -> Int
sum x y = x + y
Int -> Int -> Int
is read as "takes two parameters of type Int and returns a value of type Int".
This is a good time to mention that functions in Haskell are curried. What this means is that it's possible to partially apply functions. In many other languages if you pass fewer arguments than expected to a function, you'll get an error. In Haskell however, this is not the case. Suppose we have the function above, i.e sum which takes two arguments x and y of type Int, adds them, and returns the value. Let's say we had to add 2 with 4, 6, and 8. We could do something like this
module Main where
sum :: Int -> Int -> Int
sum x y = x + y
main :: IO ()
main = do
putStrLn $ sum 2 4
putStrLn $ sum 2 6
putStrLn $ sum 2 8
There's a shorter way of doing this utilising currying:
Cool, right?! Let's re-arrange the type signature of the sum function.
sum :: Int -> (Int -> Int)
Doesn't this look like a function that takes an Int and returns a function that takes an Int and returns an Int? It does, and that's what it is, though the brackets didn't do anything. Every function in Haskell is curried by default. This means that if you don't provide sufficient arguments, you'll get back a function that takes the remaining arguments and then returns the result. Our curried sum function in Javascript would look like:
const sum = x => y => x + y;
const sumWithTwo = sum(2);
console.log(sumWithTwo(4));
console.log(sumWithTwo(6));
console.log(sumWithTwo(8));
Alright, let's get back to our sample program; I'll copy paste it here so you don't have to scroll.
module Main where
main :: IO ()
main = do
putStrLn "Enter your name"
name <- getLine
putStrLn "Enter your age"
age <- getLine
let goodbye = "Nice to meet you"
putStrLn $ "Hello, " <> name <> " of age " <> age <> "!"
putStrLn goodbye
I think I've talked about most of this. What's left is:
- What does
IO
here mean? - What does
do
do? - Why's the name 'variable' defined using arrows, like
name <- getLine
and goodbye using=
likelet goodbye = "Nice to meet you"
? - What's
<>
? Is it the string concatenation 'operator'?
Let's try to change the value of the 'variable' goodbye
.
main = do
let goodbye = "Nice to meet you"
goodbye = "Bye"
putStrLn goodbye
Looks fine... right?
app/Main.hs:3:13: error:
parse error on input ‘=’
Perhaps you need a 'let' in a 'do' block?
e.g. 'let x = 5' instead of 'x = 5'
|
3 | goodbye = "Bye"
| ^
This is because goodbye
isn't a variable, it's called a 'let-binding', or just 'binding'. Nothing in Haskell is mutable. Once you define something, you can not change it. It helps to think of these as bindings because a binding wouldn't strike you as something you can change. In fact,
main = do
let name = "Udit"
putStrLn name
-- is syntactic sugar for
main = let name = "Udit" in putStrLn name
-- is syntactic sugar for
main = (\name -> putStrLn name) name
(\name -> putStrLn name)
is a lambda. It's the same as (name) => putStrLn(name);
in Javascript. It does certainly take some time to get used to using a backslash here, but it helps if you think of it as a lambda icon with an amputated leg (I read this somewhere myself lel). So let bindings are just syntactic sugar for an immediate call to a lambda! In Javascript this would look like (name => console.log(name))("Udit")
.
Oh, also, X is syntactic sugar for Y means that your program is converted from X to Y before being fed into the compiler. This can be done for various reasons like if you don't want to add new features to the language core but still want to implement simpler ways to do things. It sure does help here because let-bindings are nice and easy to read, and in the end it does do the same job as a immediate lambda call.
So, a few questions still remain.
- What does
IO
here mean? - What does
do
do? - Why's the name 'variable' defined using arrows, like
name <- getLine
? - What's
<>
? Is it the string concatenation 'operator'?
When I first started out, this is exactly where I got stuck. What was going on here, what was this magic? do
felt like it executed a set of statements. IO
felt like... it marked a function as an I/O user? The arrows confused me a lot. Finally, I realised what do
is syntactic sugar for. There's sure a lot of syntactic sugar in this language! It's amazing how much the do
notation abstracts away.
main = do
putStrLn "Enter your name"
name <- getLine
putStrLn $ "Hello, " <> name <> "!"
-- is syntactic sugar for
main = putStrLn "Enter your name" >>
getLine >>= \name ->
putStrLn $ "Hello, " <> name <> "!"
So now, what is >>
and >>=
?
Well, the thing is, this goes deep. In just this small sample program, we're using Monads and Semigroups. When I was starting out, I knew these existed in the language, but I thought they were things I would learn in the future when I try to make more complicated programs, but the truth is that the simplest of programs in Haskell need you to know about these concepts.
To understand this program along with every other program in Haskell, we'll need an idea about what a Monad is, for which we need an idea of what a Functor and an Applicative is, because all Monads in Haskell are also Functors and Applicatives. And finally, a bit about semigroups and monoids since they're so common but still fairly easy to grasp. These sound like big words, but, practically speaking they're not so hard once you've spent some time with them. Theoretically speaking, I have no idea, so I'll let you theoretically speak to yourself instead.
Let's first have a look at what typeclasses are, so we can move ahead to the real deal. It's a fairly simple concept.
Typeclasses
Typeclasses are somewhat like an interface. If you've used Typescript or any other language that uses interfaces, you know what they're like. The difference in Haskell is that you don't need to define the type-classes of a data-type when creating the type. If you know about traits in Rust, Haskell typeclasses are the same thing. In fact, Rust traits were inspired by Haskell typeclasses.
type Name = String
type Surname = String
data PersonName = PN Name Surname
class Splitter s where
split :: s -> (String, String)
instance Splitter PersonName where
split (PN n sn) = (n, sn)
main :: IO ()
main = do
let kath = PN "Kath" "Sandwitch"
let splitKath = split kath
putStrLn $ fst splitKath
-- outputs Kath
Here, we define a typeclass called Splitter
, which mandates that every type S that is an instance of Splitter
must have an implementation of the function split
, which takes a value of type S, and returns a tuple of type (String, String)
.
Then, we define a new algebraic data type PersonName
. Note that here, PersonName
is the type. PN
is a data constructor (function) that takes a Name
and a Surname
, and constructs a value of type PersonName
. PN
has the type Name -> Surname -> PersonName
. Name
and Surname
are type-aliases. They both mean String
.
Then we tell Haskell that our type PersonName
is an instance of the typeclass Splitter
, and provide an implementation of split :: PersonName -> (String, String)
. Note that we can destructure the algebraic data type right in the argument and then use the destructured values in our implementation. Once we've gotten the first name n
and surname sn
, all we have to do is pack them up in a tuple and send them back.
Now split
is just a normal function, but it has the type Splitter a => a -> (String, String)
. What Splitter a =>
is, is a constraint. It tells Haskell that the a
that it'll see in the following type definition must be an instance of the typeclass Splitter
. You'll get a compile error if you try to call split
with, say, an Int
, since Int
is not an instance of Splitter
.
By the way, Kath and Sandwitch are both names for a cat suggested by a friend! Imagine calling your cat Kath or Sandwitch...
One last thing before we dive deep – have a look at this
2 + 2
45 - 3
10 / 4
They're all valid in Haskell, and probably every programming language you've ever seen. There is, however, something different here – +
, -
, and /
are all just functions. Yep, no special operators that are embedded in the language. Just regular functions just like we could define like (+) :: Int -> Int -> Int
.
The reason they can be called that way is that in Haskell, all functions that only consist of symbols are by default infix. This means 3 + 2
is equivalent to (+) 3 2
(yes, you need to wrap the symbol in parantheses to call it in a non-infix way or define it). This applies not just to +
, -
and /
, but every function defined in Haskell that consists of just symbols (a lot are).
Now, we saw that functions with names composed only of special characters are infix by default, but what if you wanted to use a regular function in the infix way? Take our sum function, for example. It has the type sum :: Int -> Int -> Int
. We can wrap it in backticks to use it in the infix way – 5 `sum` 6
.
Now that we know what typeclasses and infix functions are, let's start with semigroups and monoids, since they're easier to explain and you don't need to know about Functors, Applicatives or Monads to get an idea about them.
Semigroup
In mathematics, a semigroup is a set S with a binary operation (say •) that can be applied to any two values in the set S to get a resultant value also in the set S. The operation • must be associative, which means that a • (b • c)
must be equal to (a • b) • c)
∀ a, b, c ∈ S.
Let's translate Math to Haskell – the docs define this as:
" A type a
is a Semigroup
if it provides an associative function (<>
) that lets you combine any two values of type a
into one. "
Which pretty much sums it up.
class Semigroup a where
(<>) :: a -> a -> a
This is the minimal definition of a semigroup. This means that every type that is an instance of Semigroup
must provide a function <>
that takes two values of that type and combines them. Note that Haskell does not check the associativity of this operator in any way, so it's on the programmer to define this properly.
In our example program that asks someone for their name and age and prints it with a greeting, we used <>
to concatenate strings, i.e "hi" <> "hello" = "hihello"
. This could be done because String
is an instance of Semigroup
. Another example of semigroups are lists. [1, 2] <> [3, 4] = [1, 2, 3, 4]
.
Monoid
A Monoid is a semigroup with an identity value such that id <> val
= val <> id
= val
. This is called mempty
in Haskell. The (minimal) Monoid typeclass looks like:
class Semigroup a => Monoid a where
mempty :: a
For example, for strings mempty
would be the empty string ""
, since "" <> "hi" = "hi" <> "" = "hi"
. For lists, it would be the empty list []
.
Good news - that's it! The next time you need to combine two values of the same type, check if that type has an instance of Semigroup
. If it does, you don't need to think twice before using <>
!
You might be wondering what the use of Monoids is, and why the identity value had to be defined; I'm not very clear on this as well, but I think it's so that mconcat
could be implemented. mconcat x
is just foldr (<>) mempty x
. mconcat
is a member of the typeclass Monoid, but it's created by default.
And finally it's time for functors, applicatives and monads. This might seem like unrelated theory, but it's very practical and used a lot in Haskell. I know it's hard to learn things you don't know the use for, but I can guarantee this isn't some "learn it now because you might need it later" kind of thing, but more of a "learn this now because you'll definitely use it a lot all the time you're writing Haskell" kind of thing. Things will make more sense when you make it to monads.
Before proceeding, let's define an algebraic data type: WR
.
data WR a = WR a
Let's refer to values of type WR a
as wrapped values, since after all they're just values of type a
wrapped inside WR
.
If you're wondering why we had to write WR
once more on the right side, again, the WR
on the left is called a type constructor, and constructs types. The WR
on the right is a value constructor. So we could do
something :: WR Int
something = WR 4
Here, WR
in the type definition constructs the type WR Int
, while the WR
in the definition constructs the value WR 4
.
Functors
Now, let's say we have a value WR 5
, and we want to double the value inside to make it WR 10
. How do we go about doing that?
Speaking more generically, how do we apply some function f
to a wrapped value v
?
The answer (or one of the answers) is using a functor. Functor
is a Haskell typeclass as such:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Note that a
and b
here are generic types. They could be anything. a = b
is also possible. See how we use f a
and f b
in here? This means that f
must be a type constructor that takes a type argument to create a type.
This says – for a type constructor f
to be a functor, it must define it's behavior for a function fmap
which takes as arguments:
1) A function that takes a value of type a
and returns a value of type b
2) A value of type a
wrapped inside f
and returns a value of type b
wrapped inside f
.
Sounds hard, but it's simple. Let's look at it simply then.
Let's go back to our example WR 5
. We want to double the value inside to make it WR 10
. Remember what the first argument to fmap was? (a -> b)
– a function that takes a value of type a
(which is Int
in this case), and returns a value of type b
, which we want here to be an Int
, since doubling 5, an Int, will obviously also return an Int, which is 10.
Basically we need a function that takes an int and doubles it.
doubint = (*2)
Wait, what?
This is a point-free function. It's equivalent to doubint x = x * 2
.
Point free programming refers to defining functions without having to name arguments. Arguments in this context are called points because they are the value of that argument at some point. So how does this work?
Remember when I said *
is just a function that takes two arguments? And that every function in Haskell is curried by default? This means that when we write (*2)
, we're really just constructing a partially applied *
function. It has the first argument set to 2, and now has the type Int -> Int
(well not exactly, but it'll do for now).
Some people call this pointless programming, and I can see why, since can be less readable. It is, however, perfect for occasions when it is readable.
However, it also is perfect for one more occassion - when you want to flex on someone! :D
So let's move ahead with our shiny pointless – I mean, point free, function. We now need a wrapped value. We've had this one since the start – it's WR 5
. So it's time to call fmap!
fmap doubint (WR 5) -- outputs WR 10
We did it! Though I've skipped over 2 things that I'll talk about at the end. For now, we're going a little slow on the Haskell side to allow you to have an idea of these concepts. You can try guessing what they are right now.
Note that Haskell allows you to use <$>
instead of fmap
all the time. Since <$>
is only composed of special characters, it's by default infix. This shortens our previous expression down to doubint <$> WR 5
.
I'm sure you've heard of the map
function before. For example, in Javascript, you could do [1, 2, 3].map(x => x + 1); // [2, 3, 4]
. It just applies the given function to every item of the list. Doesn't this sound familiar? If you think of a list as a wrapped value, this is exactly what fmap
or <$>
is. It takes a function that takes a value and returns a value, and applies it to the wrapped value.
Thinking this way, we can say (*2) <$> WR 5
is similar to [5].map(x => x*2)
!
This is the same in Haskell as well, with lists this time. Lists are functors, and fmap on lists is just the list map – (+1) <$> [1, 2, 3]
will be [2, 3, 4]
.
I've often heard the phrase a functor is anything that can be mapped over
, but when I heard mapped over
I could only think of the list map. This created more questions than answers for me, but I hope the explanation above could be of help. This will be more clear later, when we cover the 2 remaining things I mentioned earlier.
Applicatives
Now that you know what functors are, it shouldn't be all that hard to learn about applicatives. Applicatives do what Functors do, only that they take it one step further.
Functors apply a function to a wrapped value, while applicatives apply a wrapped function to a wrapped value.
This means that if we have a function
wrappedMultiply :: WR (Int -> Int -> Int)
wrappedMultiply = WR (*2)
and a value WR 5
, and we want to apply the function wrappedMultiply
to the value WR 5
, we'll need the power of applicatives.
Note that (Int -> Int -> Int)
is just another type, which means it fits in the generic a
.
Applicative is a Haskell typeclass as such (minimal version):
class Functor f => Applicative f where
pure :: a -> f a
<*> :: f (a -> b) -> f a -> f b
Here, Functor is a superclass of Applicative. This means that every Applicative in Haskell must also be a Functor.
Note that the only difference between <$>
and <*>
is that (a -> b)
is now f (a -> b)
!
We also have a function called pure
here, which does a very simple job – It takes a value and wraps it in f
. For example, pure 5
= WR 5
, pure "hi"
= WR "hi"
, and so on.
Let's try it out: WR (*2) <*> WR 5 -- outputs WR 10
.
That's it! I'll talk about usages of functors and applicatives later because I feel like we should now move on to monads since it's the key to understanding almost every program, and we've gotten through the important parts of the theory behind the concept of a functor and an applicative.
Monad
This is a topic that's talked about fairly often as one of the hardest concepts to grasp, and I probably have no idea about it theoretically, but I believe it's practically easy to understand. By this I mean it's not very hard to learn about and understand enough about monads to be able to write Haskell programs.
As everything else, Monad is a typeclass as such (minimal version):
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
What I mean by a minimal definition is that I've mentioned only the members of the typeclass that must be manually defined to make an instance of the typeclass in question. Other members exist, but would have a default implementation.
The Monad typeclass also consists of return
, which is supposed to do the same thing that pure
from the applicative world does. Many tutorials and websites use return
, but the Haskell wiki recommends that return
and pure
should have the same implementation, and I believe pure is better because it doesn't restrict you to Monads, and prevents confusion, since return
is used to return values in other imperative languages.
Note that the return
function here is not used to return values. Just like pure 5
would be WR 5
for our ADT, return 5
would also be WR 5
. Another benefit is that pure
is shorter and looks cooler than return
.
Also note that Applicative is a superclass of Monad, and Functor is a superclass of Applicative. This makes the chain Functor -> Applicative -> Monad, and hence it was important to understand the concept behind Functors and Applicatives first.
Now, let's talk about a very important function which you'll see every minute of doing Haskell either implicitly using do
or explicitly: >>=
, which is also called bind
or monadic bind
.
The bind function has the type (>>=): m a -> (a -> m b) -> m b
. This means it takes as arguments:
- a wrapped value of type
a
. - a function that takes a value of type
a
(not a wrapped value!) and returns a wrapped value of typeb
(which is then returned)
and returns a value of type m b
.
Essentially, bind allows you to use the wrapped value. For example, if you had WR 5
and wanted to perform a set of operations on the value 5
, you would like to get it as a value first. Bind will let you do this.
It takes a function that takes a value and returns a value wrapped in the same Monad. This means you cannot do WR 5 >>= \value -> IO 4
, since IO 4
is in the IO
monad and not the WR
monad.
main = WR 5 >>= \value -> value * 4
Note that this is also possible with fmap
and <*>
. So why do we need >>=
? The answer is chaining.
Let's create a simple program
module Main where
main :: IO ()
main = do
putStrLn "Enter name"
name <- getLine
putStrLn $ "Hello " <> name
Now let's think about imperative programming for a moment. When coding in the imperative style, you would do something like this as well. You'll set up a control flow that'll look like:
- First ask for a name
- Then get the name from user
- Then print "Hello name"
But Haskell is written in a declarative way. Or so you'll hear everywhere. When I saw a program like above, I was confused as heck, because it looks exactly like what you would do in an imperative programming language.
This is the power and beauty of Haskell – abstract notions and ideas hidden under a cozy and simple-looking structure made simple enough to allow people to utilise them for real work and not just academics.
Remember that the imperative way is just a set of instructions, and the computer is asked to execute them in order. "do this, then do this, then do this". They're not connected; they're just individual commands that're being executed in order.
The Haskell version might look like this too, but let's desugar it and see what it actually is (the do
notation is just syntactic sugar, it's converted to another form before being passed to the compiler):
Yes, it's just monadic binds. We're only using functions – no "control flow" is being fed into the computer. No unrelated set of commands that we want the computer to "just execute in order". Just function calls and nothing else.
Whenever you see infix symbols, remember that these aren't some special syntax and are just regular functions being used in the infix way.
We see a new thing here: >>
. This isn't anything new – it's just the monadic bind for when you don't care about the value inside. For example, something = WR 5 >> WR 10
will be WR 10
. This is the same as something = WR 5 >>= \someValue -> WR 10
; you aren't using someValue
so might as well just use >>
to not have to create binds without a reason.
I'll once again try putting into words the >>=
function: it's a function that takes a value of type a
wrapped in some Monad m
and another function (say f'
) that takes a value of type a
and returns a value of type b
wrapped in the same monad m
. What you get back from >>=
is what you get from f'
.
This allows us to chain expressions together and define the way a program is supposed to work.
You still might be confused about Monads, but I'm sure this'll go away once you try making some programs and practice a bit more.
Alright, we also left something very important for later: IO
.
IO
isn't very different from our type WR
(it actually is, but this way of thinking will do for now).
To understand the concept behind IO
, we first need to go back to when I said 'Haskell is a purely functional programming language'. In Haskell, every function is a pure function that has no side effects. A side effect is something that changes or accesses some global state which may result in the output of it being different for the same input among multiple runs.
I/O is also a side effect, since for example, consider you have a function that reads a file. Reading a file is I/O. This function takes an argument which is the file name, and returns the contents of the file. This function is not a pure function, since it's output for the same argument can change depending on the contents of that particular file in the filesystem.
Printing to screen and so on is also I/O and a side effect, since we return an empty tuple from, say, putStrLn
, but we're printing to screen, which is a side effect since we're performing some extra action which we aren't returning.
So how can we say that Haskell functions are pure despite being able to do I/O? There's a little trick involved here. Every function that does some IO returns something wrapped in the IO
monad (IO a
). For example, putStrLn
returns IO ()
and getLine
returns IO String
.
The idea is that when we call the function that prints something on the screen, it returns the state of the world we live in – every atom, every particle, the state of all matter after we've printed that thing onto the screen. When we print another string, that state is replaced with the state of the world after the second string has been printed to the screen. Remember that our main
function returned a value wrapped in IO
? That's right. main
is a pure function because it returns the new state of the world with all the IO that we did back to the world.
Let's now have a look at the arrow binding syntax.
main :: IO ()
main = do
let something = putStrLn "Hello"
putStrLn "Bye"
This will just output "Bye". This is because something
here is just a binding of type IO ()
. In order to actually evaluate this, you need to use <-
this way:
main :: IO ()
main = do
let something = putStrLn "Hello"
val <- something
putStrLn "Bye"
Now you'll see both "Hello" and "Bye" printed on the screen. Here, val
will now have the type ()
. Note that it's no longer wrapped in IO
! this is because it's actually an argument to the bind function, since do
notation is syntactic sugar for a series of binds like we saw above.
Effects are only ran when the wrapped value is 'unpacked'. It could be unpacked using the monadic bind, or using <$>
or <*>
, otherwise you just have a wrapped value that you don't know anything about until you 'unpack' it.
You can only use the arrow if the function on the right side returns some value wrapped in the same monad, IO
in our case. This means that every function called inside a do
block must be in the same monad (as in must return a value wrapped in the same monad). You can define let bindings that can contain any value though. This is because let bindings aren't chained with >>=
or >>
, they're just a named set of expressions or calls. If you never use a let-in binding, it will never even be evaluated.
The Monad typeclass also consists of a function called join
, which has the type m (m a) -> m a
. This just "removes one level of the monadic structure" like the wiki says. For example, join $ WR (WR 5))
will be WR 5
.
An interesting thing to note is that bind (>>=
) can be derived from fmap
and join
. Left as an exercise, since this doesn't really matter as much practically, but is interesting to know about and not so hard.
I believe I've talked about enough to let you get started with Haskell. I said when I was talking about Functors and Applicatives that I've left some things. This was the actual implementation of <$>
and <*>
for our type WR
. This isn't so hard, since it only is a wrapper for one generic value.
data WR a = WR a deriving (Show)
instance Functor WR where
fmap f (WR v) = WR $ f v
instance Applicative WR where
pure = WR
(WR f) <*> (WR x) = WR $ f x
The two things I left out were
- the implementation of
fmap
and<*>
- the
deriving (Show)
part
I've talked about the first point before so I'll skip over it.
Here, deriving (Show)
just tells Haskell to automatically derive the implementation of whatever is required to make WR
an instance of the typeclass Show
. Show
is needed if you want to be able to print
values onto the screen.
print
is a function I haven't talked about before, but it's pretty simple. We've seen that putStrLn
prints strings onto the screen. print
prints anything that is an instance of Show
onto the screen. As simple as that.
There's something else I would like to clarify that might sound confusing even though we've talked about it before: pure = WR
. If you remember pure
, it just lifts a value into a monad, for example pure 5 = WR 5
in the context of WR
. WR
here is the data constructor for WR
of type a -> WR a
. It hence makes perfect sense to do this.
Pattern Matching
Let's define our familiar tree ADT again:
data Tree a = Node a | Branch (Tree a) (Tree a) deriving (Show)
Now, what if we wanted to make this into a functor? Suppose we wanted to add all values in a tree. Let's define an Int tree.
/\
/ \__
/\ /\
3 1 / \
6 /\
/ \
2 9
Holy shit making a tree with dashes and slashes is hard. Anyways, ignore that horizontal line, it's just a regular tree.
So, what if we wanted to add something to all the values of this tree? But not just this tree. Any Tree
.
One idea is to use fmap
. This certainly gives me the feel of "mapping over" the tree. Let's do it.
instance Functor Tree where
fmap f (Node val) = Node $ f val
fmap f (Branch t1 t2) = Branch (fmap f t1) (fmap f t2)
Now this is a pattern match. Note that we're defining the same function on different lines, with the difference being in the Tree
value. Since Tree
can be either of these, we can write it this way.
There's also a more verbose way to pattern match using case
:
instance Functor Tree where
fmap f x = case x of
Node val -> Node $ f val
Branch t1 t2 -> Branch (fmap f t1) (fmap f t2)
Also note that this is a recursive call. If we encounter a node, we just apply the function f
to that node. If we encounter a Branch
, we apply the function f
to both branches. If one of those branches is a branch too, the same happens.
Let's try this out!
data Tree a = Node a | Branch (Tree a) (Tree a) deriving (Show)
instance Functor Tree where
fmap f x = case x of
Node val -> Node $ f val
Branch t1 t2 -> Branch (fmap f t1) (fmap f t2)
ourTree :: Tree Int
ourTree = Branch (Branch (Node 3) (Node 1)) (Branch (Node 6) (Branch (Node 2) (Node 9)))
main :: IO ()
main = do
print $ (+10) <$> ourTree
This outputs Branch (Branch (Node 13) (Node 11)) (Branch (Node 16) (Branch (Node 12) (Node 19)))
– we did it!
I left the usage of Functors and Applicatives for later as well, so let's have a look at one of the more common usages that I've seen over the course of the previous few months. This specific usage involves the use of <$>
and <*>
to construct an ADT. This is common while streaming – you could be getting certain values needed to construct the ADT through the network or the fs, and this would help. Here I'll just use getLine
to demonstrate.
type Name = String
type Surname = String
type Age = String
data Person = Person Name Surname Age deriving (Show)
main :: IO ()
main = do
putStrLn "Example use of <$> and <*>"
person <- Person <$> getLine <*> getLine <*> getLine
print person
Let's run it:
Example use of <$> and <*>
> Udit
> Karode
> 20
Person "Udit" "Karode" "20"
It works! (I added the "> " to make it more clear that those were inputs).
We make use of currying here once again. By now it should be clear that currying is a very useful thing in Haskell. It gives us more flexibility and freedom.
So when you do Person <$> getLine
, you get a value of type IO (String -> String -> Person)
. This is because initially Person
had the type String -> String -> String -> Person
, and we fmap'd it with IO String
that we received it from getLine
. This called Person
with the String from the IO String
we got from getLine
, and put the result into IO
.
Note that we now have a function wrapped in IO
, and want to apply more IO
values to it (IO String
s that we get from getLine
). Hence, we need to use <*>
from now onward.
We could do the same thing using do
notation:
type Name = String
type Surname = String
type Age = String
data Person = Person Name Surname Age deriving (Show)
main :: IO ()
main = do
putStrLn "Example use of <$> and <*>"
name <- getLine
surname <- getLine
age <- getLine
let person = Person name surname age
print person
This is a more pointful way of writing the same program. It's longer, but it's fine. The functor-applicative way of doing the same thing, however, is more succint, and should be preferred.
I'll provide examples of two monads that are very widely used in Haskell for error handling: Maybe
and Either
.
Maybe a
can be either Just a
or Nothing
. Either a b
can be Left a
or Right b
. This is useful in situations where something can error. It's standard in Haskell to denote error messages (or any error value) using Left
and successes using Right
.
The wonderful thing about using Maybe
and Either
is that errors cascade.
Let's see an example to show what I mean.
checkKind :: String -> Either String String
checkKind "human" = Left "Humans are not allowed in the animal zoo!"
checkKind _ = Right "Welcome to the animal zoo!"
checkNumberOfLegs :: String -> Either String String
checkNumberOfLegs "0" = Left "Zero-legged creatures are not allowed."
checkNumberOfLegs _ = Right "Have fun!"
checkBoth :: String -> String -> Either String String
checkBoth kind legs = do
kindMsg <- checkKind kind
legsMsg <- checkNumberOfLegs legs
Right $ "Verification complete! " <> kindMsg <> legsMsg
main :: IO ()
main = do
putStrLn "What's the kind of your species?"
species <- getLine
putStrLn "What's the number of legs you have?"
legs <- getLine
case checkBoth species legs of
Right msg -> putStrLn msg
Left msg -> putStrLn msg
_
here is just some value we don't care about.
We've used a pattern match on String
that checks for ""
, and any other String _
in checkKind
and checkNumberOfLegs
.
What do you think will happen if we say kind is human
and legs are non-zero?
What's the kind of your species?
> human
What's the number of legs you have?
> 2
Humans are not allowed in the animal zoo!
The number of legs were never even checked. This is because Nothing >> Just something = Nothing
, and Left something >> Right something = Left something
.
This means that if one expression in the do
block evaluates to Nothing
or Left
, every expression from then onwards becomes Nothing
or Left
upto the return value. This allows to have a huge stack of function calls where we can simply and easily propogate errors throughout till the point where we use those functions on the outer-most layer. It's like a break
or early return in imperative languages but way more powerful and customisable.
I think that's more than enough to get you going with Haskell, but there's still much more to know about Haskell. For example, we have been chaining types using ->
in type definitions, but did you know that this isn't some special magic syntax, but just a type constructor? It amazes me how simple Haskell can be at times.
I'll now recommend making some basic programs to get a greater idea of Haskell. You can then read the following to get a better idea of everything:
- Learn you a Haskell by Miran Lipovaca
- What I Wish I Knew When Learning Haskell by Stephen Diehl
I'll borrow some words from "What I Wish I Knew When Learning Haskell":
As a programming language, Haskell pushes the frontiers of programming language design more so than any other general purpose language while still remaining practical for everyday use.
Beyond language features, Haskell remains an organic, community-driven effort, run by its userbase instead of by corporate influences. While there are some Haskell companies and consultancies, most are fairly small and none have an outsized influence on the development of the language. This is in stark contrast to ecosystems like Java and Go where Oracle and Google dominate all development. In fact, the Haskell community is a synthesis between multiple disciplines of academic computer science and industrial users from large and small firms, all of whom contribute back to the language ecosystem.
Until next time!