By reading this article you will gain a basic understanding of the Haskell programming language and the functional programming paradigm.
First of all… Why Haskell?
Haskell is THE language when it comes to functional programming because it embraces this programming paradigm fully while still being somewhat usable for real-world production applications.
Yes, it does have a reputation for being academic & very hard to learn but this is only because most people are not used to thinking about pure functions, recursion, immutability & the famous Monads.
The truth is that it does take effort to learn, but that’s the same for pretty much everything worth knowing so I encourage you to give it a good try & not give up at the first sign of difficulty.
With that said… Why functional programming?
I have already mentioned some of the features that this programming paradigm brings to the table, like pure functions, which are exactly the reason to learn functional programming.
You’ll learn a different way of thinking from what you’re used to, which will open the doors for creativity & understanding beyond what you imagined could be possible.
There is also several practical advantages, for example, functional programs are more predictable because side-effects are explicit, so there is no surprise state updates leading to unpredictable behavior.
Haskell in particular also implements a strict typing system, so you know what types of values a function takes as input and returns as output.
In other words, the compiler won’t let you call a function that needs a string with an integer as input, which eliminates a whole category of runtime programming errors.
Hopefully, all of this is enough to convince you to give it a try, so let’s get into it.
Installing Haskell
To start writing Haskell code you’ll need to install the compiler, known as GHC (Glasgow Haskell Compiler).
As always, there are different ways to do this:
- Using your system package manager (for example
apt
), the main disadvantage is that you’ll get older versions & it may not integrate as well with other tools (editor plugins). - Using a version manager like
asdf
& the appropiate plugin asdf-haskell. - Using ghcup the official Haskell installer, which manages multiple GHC versions (like Ruby’s rvm) & HSL (Haskell Language Server). This is the recommended method!.
When you have decided on your install method & proceeded with the installation you can try to launch ghci
(Haskell interactive compiler) which is the equivalent to irb
in Ruby.
If ghci
opens fine then you can proceed to the next section.
Let’s write some code!
Basics of Haskell
Because functional programming is all about functions let’s introduce the syntax for writing a simple add
function that adds two numbers together.
Here it is:
add x y = x + y
And the Ruby equivalent:
def add(x, y) x + y end
Notice the lack of commas between arguments & the lack of parenthesis in the Haskell version.
To call the Haskell function you can write this:
add 5 3
As you can tell in Haskell we use spaces to separate function arguments, and there is no need for parenthesis (but they can be used to specify evaluation order).
There is more than meets the eye, but for now, I would like to introduce another concept: type signatures.
add :: Int -> Int -> Int
The convention is to write the type signature before the function definition, and while they aren’t always required for your program to compile (there is a type inference system) it’s often a good idea to include them.
Here’s how to understand the type signature:
add
= The name of the function.::
= This is the syntax for declaring a type signature.Int -> Int -> Int
= This is saying that the function requires two values of typeInt
& it will return a value of typeInt
.
It can be a bit confusing at first, but the key here is that the last type in the list is always the return type for the function & anything before that is the function argument types.
Let’s look at another function together with its type declaration:
isEmpty :: String -> Bool isEmpty str = null str
This particular function checks if a given string is empty, from the type declaration we can see that to call the function we need a string & as a result we’ll always get a boolean value (True
or False
).
Btw null
is a built-in Haskell function as you probably already figured out.
Haskell type declaration system supports polymorphic types, in other words, we can define a wildcard type as either an input or output value.
Example:
isListEmpty :: [a] -> Bool isListEmpty xs = null xs
In this example, we’re defining a function that takes a list ([]
) of elements of type a
, which is a polymorphic type.
It means that a
could be String
, an Int
, a Bool
, or any other valid type so this gives us more flexibility when it comes to writing functions.
Notice that in Haskell lists are a linked-list data structure, so they are different from Ruby arrays, and all the elements stored in a given list must be of the same type (for example a list of strings, but you can’t mix strings & ints like in Ruby).
So that’s the basics of functions, now you’ll learn about pattern matching, a very powerful feature that is also available in recent versions of Ruby but isn’t as important to the language.
Keep reading to find out why!
Pattern Matching
Pattern matching allows you to search for either a specific value or a value that conforms to a particular structure.
For example:
isListEmpty :: [a] -> Bool isListEmpty [] = True isListEmpty xs = False isListEmpty [] -- True isListEmpty [1,2] -- False
This is exactly like the isListEmpty
function we defined before but now instead of using the built-in null
function, we use pattern-matching.
Let’s analyze how it works.
On line 1 we have our type signature which says that we require a list ([]
) with elements of some arbitrary type (a
) & as a result we’ll get a Bool
.
On line 2 we pattern match against the empty list & the way we do this is by declaring the empty list as the argument, then if there is a match this function definition will be used (return True
).
On line 3 we pattern match against the non-empty list (xs
), then if there is a match we’ll return False
.
Lines 5 and 7 correspond to the function calls & in the following lines, we can find the output.
The super cool thing here is that there are no if statements in this code (but of course they are available in Haskell). Our function knows what to do solely based on the form of our input values.
[!INFO] Order Matters!
You should know that the order in which you define your functions matters, if you switch the order of this function (making the empty pattern match version second) it will always returnFalse
.
That’s the power of pattern matching!
Now you may be wondering about this xs
, it’s nothing special, just a common variable name for lists in Haskell.
When you use a variable it will just match anything, but because of the type system, it will only allow lists for this particular function we just defined (isListEmpty
).
Let’s look at how you can use pattern matching to “deconstruct” a list.
first :: [a] -> a first (x:xs) = x first [1,2,3] -- 1 first ['a', 'b', 'c'] -- 'a'
Here we use the pattern (x:xs)
, which will match a non-empty list & “save” the first element as x
& the rest of the list as xs
.
You may have noticed that we don’t use the value for xs
, in this case, it’s considered good practice to use the wildcard character _
to indicate that this variable is unused in the function definition.
Like this:
first :: [a] -> a first (x:_) = x
This doesn’t change how the function works, it just discards the unused value.
Again, there isn’t anything special about the name x
, you could use any other valid name.
This is what you need to know to to write some basic Haskell programs, so let’s move on to the next topic & keep learning.
Dealing with Immutability
So far we haven’t seen anything that should be too confusing or groundbreaking if you’re coming from an OOP language.
But immutability is a big one.
How do you even get anything done if you can’t just stuff data into an array or hash & change it freely?
How do you even handle any kind of state, even as simple as a counter?
Well, let’s look at this.
While you can change a variable value once it’s defined (you can forget about trying i += 1
, for example) it’s still possible to write real programs in a functional programming language.
It requires a shift in thinking, instead of thinking of mutating a value, we want to think about what steps are required to build up to the final value.
Let me explain.
If we want for example to write a function that counts how many elements are in an array, normally we would iterate over & do +1 on some counter variable.
In functional programming what we’re going to do is go over the list by means of recursion & keep adding one, but instead of adding one to a variable we just build a chain of +1 function calls which when the recursion resolves will look something like 1 + 1 + 1
for a list of size 3.
It may sound complicated without looking at some code, but it isn’t, it’s just a different way to think about it.
Before we can see the code we need to talk a bit about recursion.
Recursion
In Haskell, the main means of traversing a data structure is using recursion instead of iterations.
- Recursion = A function that calls itself.
- Iteration = A loop that runs a given amount of times (think
each
method in Ruby or even thetimes
method).
If you never dealt with recursion before this is a whole topic that you’ll need to devote a bit of quality time to understand.
Let’s look at the example of counting elements:
count :: [a] -> Int count [] = 0 count (_:xs) = 1 + count xs
You probably noticed that we’re using pattern matching here, and the reason is that it’s integral to making this work.
This will match the 2nd version of the function (line 3) if called with a non-empty list & do 1 + count xs
where xs
is all the elements of the list but the 1st one (which we’re discarding in this case because we don’t need it).
Now here is the beauty of this, by calling again count
, as long as the list is not empty it will keep matching this function definition so essentially we’re building a chain of 1 + 1 + count xs
.
We just keep adding 1
, but the actual addition won’t resolve until the list is empty which will match the empty list case (line 2) & return 0
.
So for count "abc"
the recursion will resolve as 1 + 1 + 1 + 0
& that, of course, evaluates to 3
which is the correct output.
Isn’t that cool?
Yes, I understand that it may take some time to wrap your head around this, but you need to give it time & think about it.
Here are some important points:
- The key is in the pattern matching, remember that order matters
- Another important point is that we’re “trimming” the list (strings are also lists in Haskell) after every function call, that’s how we’re progressing toward the “base case”.
- Why do you think we need the 1st case (empty list)?
- Because if we call
count []
we want the result to be 0. - Because all definitions of the function MUST match the type signature, even if using pattern matching.
- Because if we call
- The way this code is evaluated is by replacing the recursive function call (
count xs
) with the right-hand side of the matching function, so1 + count xs
becomes1 + 1 + count xs
given thatxs
is not empty. This keeps going until the recursion is done (empty list).
Hopefully, it makes sense after a while, if not try looking a other examples out there or try changing the code & see what happens 🙂
Basic I/O
The observant reader will probably wonder where is the classic “Hello World” example that most programming introductions start with.
There is a simple answer, because Haskell purity means we can’t just do I/O operations whenever we want & writing to or reading from the terminal is an I/O operation.
Pure functions forbid us from messing around with any kind of external environment, and for a while, when Haskell was originally written, there was no I/O.
However the designers of Haskell decided they wanted to make it a practical language & not just an academic one, so they found a solution.
The solution is the fancy-sound “Monad”.
We won’t go into detail, for now, it’s enough to know that monads are like a design pattern with mathematical underpinnings (that’s where the name comes from).
Now, we have the IO Monad which allows us to encapsulate I/O as actions that will be eventually run by the Haskell runtime.
So if we want to print something to the screen the easiest way to do it is by using main
, the entry point function for Haskell programs.
Here’s an example:
main :: IO () main = print "Hello World"
There you go, that’s your hello world in Haskell, but the reason I didn’t open with that is that you can’t use print
in any function you like, it has to be a function that specifically returns an IO action.
In the type system, we can declare that like this: IO ()
.
So if you want to print something the usual pattern is to do it from the main function because its type signature says that it will return an IO action.
It gets a bit more complicated, if you want to use multiple print
expressions you’ll need a do
block.
Like this:
main :: IO () main = do print 123 print "Hello World"
This do
is not related at all to Ruby’s do
, it’s completely different, what it does is combine multiple IO actions in a sequence.
It has a lot to do with that “monad” thing I mentioned earlier but the details are out of scope for this article.
One last thing, indentation in Haskell matters because it helps the compiler know where your do
block begins & ends.
Eventually, you’ll also learn about let
+ in
expresions & where
expresions, where indentation is also relevant.
Lazy Evaluation
Another concept that can take some time to wrap your head around is “lazy evaluation“.
Most programming languages use “eager evaluation“, meaning that all the expressions will be evaluated fully as soon as possible, even if they are not needed.
This means that if you have a function that generates a list of a million items, but you only need the first 10, an eager language will still generate the whole list, but a lazy language will only generate what it needs.
Example:
take 10 [1..]
Where [1..]
is an infinite list & take
is a built-in method that will extract 10
(in this example) elements from that list.
This is only possible because of the lazy nature of the language.
Something Familiar: Higher-Order Functions
I want to end this introductory article with something you’re already familiar with, functions that take other functions as arguments.
This includes functions like map
& filter
.
Here’s an example:
map (+1) [1..10] -- [2,3,4,5,6,7,8,9,10,11]
In this case, the function is +1
, which is a partially applied +
function.
All functions with more than one argument can be partially applied & you get a new function as a result.
Ruby version:
Array(1..10).map { _1 + 1 } -- [2,3,4,5,6,7,8,9,10,11]
In this case, the function is { _1 + 1 }
, which is a lambda that takes each element of the array & adds one to it.
As you can see the Haskell version is more clear & succinct.
That’s one more reason to love it!
Combining Functions
Let’s say you want to call a function with the result of calling another function.
You’ll need to use parenthesis or the function application operator ($
).
Like this:
filter even $ map (+1) [1..10] -- [2,4,6,8,10] filter even (map (+1) [1..10]) -- [2,4,6,8,10]
The two versions are the same, the $
version is usually preferred.
Conclusion
The whole purpose of this guide was to introduce you to the world of functional programming & to pike your curiosity so that you’ll go on your own and keep learning.
There is a lot more, but this should give you a little taste.
One last question…
Should you ditch Ruby for Haskell? Of course not! Haskell can be a pain to get anything done, but it’s still an interesting challenge which will teach you a different way of writing code.
Thanks for reading & have a nice day! 🙂