Intro to Haskell
Updated: 17 November 2024
Notes based on Learn you a Haskell
Prerequisites
Setup a Haskell Environment as per the Getting Started Docs as well as install the Haskell extension for your editor
This installation process is a little slow, but stay with it I guess. (it’s still easier than the Ocaml install)
Syntax Basics
To write the first bits of Haskell we can use the REPL which, once installed, can be accessed with the ghci
command
Math can be done in a fairly normal fashion:
And booleans similarly:
And equality:
All these basic operators are functions in Haskell that work using infix notation. We can also refer to them purely as a function by wrapping them in brackets, e.g. (+)
or (==)
Functions can be called using the name of the function followed by the arguments, separated by spaces
Or with multiple arguments
If a function takes two parameters we can call it using infix notation with backticks surrounding the function name, for example the min
function:
Functions
We can define our own functions using a syntax like:
We can put that into a file, e.g. called double.hs
, and load it into the REPL with :l
:
We can then use the function that was defined
A function that takes multiple arguments can be defined by separating the arguments by a space
In the above cases, Haskell is able to determine the types of our function as: doubleUs:: Num a => a -> a -> a
We can make a function that does some conditional stuff using thenif .. then .. else
expression
Note this is not whitespace sensitive
Functions that take no parameters are also called definitions or names, we can defined a function that is equal to some string value as follows:
Function names must start with a lowercase letter and can contain a
'
, e.g.bob'sName = "BobSmith"
Lists
Lists store elements of the same type. Lists can be defined using []
with elements separated by commas
They can also be concattenated with ++
++
can also be used with strings:
When concatenating elements Haskell needs to traverse the entire list which can negatively impact performance. An alternative way of adding an item to a list is the the cons (:
) operator:
Elements can be accessed from a list using !!
Lists can also be compared. Comparison of lists are done item by item, for example a list
Some functions for working with lists are:
head
- gets the first itemtail
- gets everything other than the first elementlast
- gets the last iteminit
- gets everything other than the last elementlength
- gets the length of a listnull
- checks if the list is emptyreverse
- reverses a listtake
- takes a certain number of elements from a listdrop
- drops a certain number of elements from a listminimum
- gets the smallest element of a listmaximum
- gets the largest element of a listsum
- adds up items of a listproduct
- multiplies all numbers in a listelem
- checks if an element is in a list, usually called as an infix method since it’s more readable that wayreplicate
- produces a list by repeating some value a certain number of times
Calling head
, tail
, last
, or init
on an empty list will throw an error
Ranges
Ranges allow us to define sequences of enumerable values. Ranges can be defined using a start and end value
They can even be defined using the first two values in the case where we want to use a step larger than 1
Additionally, ranges can also be infinite by not specifying the end of the range. We can see the result of doing this by taking some elements from the range as below:
There are also a few useful functions for working with ranges such as:
cycle
- which repeats the elements of a given list to produce an infinite rangerepeat
- creates an infinite range of a repeated value
List Comprehensions
List comprehensions are like Set Comprehensions in math and allow us to define some general sets using a set of predicates
A set comprehension follows the structure of [ expression | variable <- values, predicate]
. This is more clear in practice:
These can also be used with multiple variables in order to produce permutations of a multi-variable expression
Tuples
Tuples work pretty much the same as in other languages. A tuple lets us store multiple values together. Items in a tuple can be different types, we can use this as we’d expect
When a tuple has only two elements we refer to it as a pair
. Pairs have some methods for working with them, namely:
fst
- gets the first item from a tuplesnd
- gets the second item from a tuple
It’s not too difficult to implement these functions, an example implementation may look like so:
And we can compare our usage with the base implementation
Another nice function for working with tuples is zip
which takes two lists and iterates over them and outputs a list of tuples
Types
Haskell is statically typed. Thus far we haven’t had to annotate the types of our functions since the language is pretty good at inferring the types of stuff
We can use :t
in the REPL to view the type of something
We can specify the types of a function by writing it above a function implementation, for example:
Type Variables
We can look at the types of some more complex values
In the above functions, we can also see that some functions make use of a type variable, these types are denoted by the lowercase character in the type signature as in fst :: (a, b) -> a
.
We can also see that functions can specify a constraint on their type variables, for example (+) :: Num a => a -> a -> a
constrains a
to be of type Num
. The constraint is denoted before the =>
symbol in a type definition
Lastly, it’s also important to note that functions don’t differentiate between multiple inputs an outputs. So a function that takes multiple input just uses ->
to separate values, this is because functions can partially apply them which yields another functions
Type Classes
A typeclass is like an interface that defines some kind of behavior. (They seem a bit like traits in Rust)
Some common typeclasses are:
Eq
- which makes something comparable with==
or/=
Ord
- used for ordering, makes something comparable with>
,<
, or==
. So implementation ofOrd
requiresEq
as wellShow
- means that something can be printed usingshow
Read
- means that something can be read from aString
into some type with theread
function
The read
function is interesting because it lets us read a value into multiple different types. In cases where this type cannot be inferred you can specify the type that something needs to be read into using :: Type
which lets us provide a value to a type variable
Additionally, we also have some number types:
Num
- the base number typeInt
- a bounded integerInteger
- a non-bounded integerFloat
- a single precision, floating point numberDouble
- a double precision, floating point numberEnum
- an enumerable value, these can be used in ranges
When working with numbers the fromIntegral
function can be used to convert between number types, namely swapping between floating point and integer number types which is sometimes needed
Functions
Functions in Haskell have some interesting functionality when compared to most other languages. The first of this is how we can define different implementations for functions using pattern matching
Pattern Matching
Pattern matching when defining a function is done by essentially providing multiple implementations for a function that match to the same type signature but different concrete parameters. For example, the below function reacts differently depending on the name
that is provided:
And when using this we can see how the inputs are handled:
We can use this idea to implement something like an implementation of the triangular number calculation that uses a but differentiating the recursive base case as a pattern
And we can use that to get the first 10 triangular numbers:
Pattern matching can also be done in more interesting ways, for example we can use it with tuples like so:
And similarly for lists, for example:
When defining patterns, it’s important to keep the most specific patterns first, since they are matched in order (the compiler will warn you if your order is incorrect so that’s nice). Note that non-exhaustive patterns will be a runtime error so it’s important to ensure that we handle these as well
Another nice benefit of pattern matching is that we can use it for destructuring elements, for example with a tuple
Guards
Guards allow us to branch based on a condition. They’re very similar to if/else
statements and are evaluated in order
A simple guard can check some conditions:
Where Bindings
Where bindings let us define local values within a function and can be done by using where
with each binding on a different line:
These are also usually indented for readability
Where bindings can also pattern match just as we would when defining a function:
Let Expressions
These are very similar to where bindings but can be used anywhere and are expressions in themselves, the values in the let
part are accessible in the in
part
Otherwise, they work as you’d expect
Or with pattern matching:
An important distinction is that let expressions can be used in any place where we may need an expression, and evaluate to the in
part
You can even define functions in them, of course:
You can also use it in the predicate section of a list comprehension and do stuff with that:
This seems a bit syntax-dense to me, but I see why being able to refer to the value bound in the let expression could be useful
Case Expression
Case expressions let us pattern match within a function and get the resulting expression. This is pretty much as you’d expect as well
Pattern matching in function definitions is actually just syntax sugar for case expressions so you can define the name
function using a where binding with which would be the same as a case expression:
Recursion
Recursion works as normal. A nice benefit we have in Haskell is that we can define the recursive base case as a pattern definition:
We can also define infinitely recursive functions, for example:
And because Haskell supports infinite lists, we don’t have to have an escape condition. This is useful and can be composed with other things, for example:
And that just works as expected
Higher Order Functions
Functions that can take a function as parameters or return another function are called higher order functions
Curried Functions
Every function in Haskell only takes a single parameter. Functions that look like they take multiple parameters are in fact functions that return functions that take the remaining parameters. We can call these curried functions
An example of this is the max
function, it can be used in the following two equivalent manners:
This is because max 1
returns a function that takes the second value. This idea lets us partially apply functions that require multiple parameters, for example:
In the above, we can see that the with1
and with2
are partially applied versions of the triple
function in which a
or a
and b
are provided
Infix functions can also be applied partially using sections - a section is done by surrounding the function with parenthesis and putting a parameter on one side. This means that we can also apply it differently depending on how we want to use it, for example:
It’s clear when we use it how this works:
Higher Order Functions
We can define higher order functions as normal functions
We can then provide it with a function:
A very useful higher order function is flip
, which is basically flips the arguments to a function:
Maps and Filters
Two common higher order functions are map
and filter
map
- applies a function to each value of a listfilter
- returns a list with only items that return true from a predicate function
These functions are defined in the standard library. A basic definition for them might look as follows:
Or even using list comprehension:
And using it as such:
As we can see from the implementation, this behavior can be done using list comprehension pretty much directly. The usage of each really just depends on what is more readable ina given scenario
Since Haskell is lazy, mapping or filtering lists multiple times still only iterates through the list once
Lambdas
When working with higher order functions we often have a function that we’d just like to use once, to do this we can define a lambda which is an anonymous function. They are defined using the \p1 p2 -> expression
For example:
Often we need to surround them in parenthesis since they will extend to the end of the line otherwise. Using it is done as follows:
Folds and Scans
When working with recursion we often run into an edge case with the empty list, this is a pretty common pattern called folding. Folds reduce a list to some single value
We can use the fold-left method foldl
to implement a sum
function:
foldl
takes a lambda to handle the accumulation, the initial value, and the array. Note that for the above case in which we’re writing the sum
function, it can be replaced more simply as:
The same idea can be applied to the elem
method:
Equally, you can use a fold-right which does the same but iterates from the right. Additionally, the lambda arguments are flipped around:
Folds are pretty powerful and can be used to implement lots of other standard library functions
A nice compositional example is how we can use these methods with flip
to do something like reverse a list:
Function Application
Function application is done using the $
operator and is defined as such:
The purpose of this is to reduce the precedence of function application. This means that instead of doing e(f(g x))
we can do e $ f $ g x
So we can rewrite something like:
As
Thus, effectively reducing the precedence of function application
Additionally, this can also be used to apply a function dynamically, for example:
Function Composition
Function composition is defined mathematically as (f.g)(x) = f(g(x))
. In Haskell this is the same:
Using this, we can convert:
Into:
Or using what we just learnt about the function application:
This mechanism also makes it possible to write functions in a more point-free style, for example: