Haskell
Haskell is a statically typed, purely functional nonstrict-evaluation programming language. Informally, static typing means that we can look at a program (without executing it) and tell what the type of any term is. A purely-functional language is a language that supports only functional programming concepts (unlike multi-paradigm languages like Python). Nonstrict-evaluation means that there is no strict sequence of evaluating statements or expressions, and compilers are free to decide which expressions should be evaluated first—lazy evaluation is where expressions are evaluated only when they are needed. We will look at non-strict evaluation eventually; for now, understanding static typing and purely functional programming is more important.
In a purely functional language like Haskell, you will miss the following programming language features that are present in virtually every general-purpose programming language:
- Mutation (even variables are immutable);
- Loops;
- Objects (classes etc.);
- Dynamic typing (e.g.
x
can be anint
now, and astr
later);
You might find it difficult to adjust to such a programming environment. However, you will find these restrictions meaningful as we have alluded to in the previous section.
Basic Expressions
Basic Math
By this point you should have already installed GHC, which comes with
two main parts: ghc
itself (the compiler), and ghci
the
REPL/interpreter. For now, run ghci
in the terminal to start an interactive Haskell shell, similar to how we run python
in the terminal to start an interactive Python shell:
~ $ ghciGHCi, version 9.4.8: https://www.haskell.org/ghc/ :? for helpghci>
~ $ pythonPython 3.13.2 (main, Jan 1 1970, 00:00:00) [GCC 14.2.1 20250128] on linuxType "help", "copyright", "credits" or "license" for more information.>>>
and enter some basic mathematical expressions!
ghci> 1 + 2 - 30ghci> 1 * 2 / 40.5ghci> 5 ^ 2 `mod` 50ghci> 5 `div` 22
>>> 1 + 2 - 30>>> 1 * 2 / 40.5>>> 5 ** 2 % 50>>> 5 // 22
Note some differences: ^
is exponentiation (just as you would normally type in a calculator), and there is no modulo operator. There is a modulo function called mod
similar to %
in Python. Integer division is a function div
, similar to //
in Python. The usual operator precedence rules apply.
Everything is a Function
In a functional programming language like Haskell, it should come as no
surprise that virtually everything is a function. Mathematical operators
are actually just functions! In GHCI, we can observe the type of any
term (terms are sort of like objects in Python; functions are terms!) using :t
, and we can show the type of
the function of the +
operator by issuing :t (+)
(when writing
operators as a term in the usual prefix notation, surround it in parentheses).
We can in fact re-write an infix operator function call as a normal prefix
function call. Note that in Haskell, f x y z
is essentially
the same as f(x, y, z)
in languages like Python.
ghci> :t (+)Num a => a -> a -> aghci> 2 + 35ghci> (+) 2 35
As we know, currying is the act of translating an (+)
really
looks something like this in Python:
def add(x): return lambda y: x + y
This is automatically done in Haskell. Thus we might be able to write
our Python equivalent of add(2)
directly in Haskell as
(+2)
:
ghci> y = (+2)ghci> y 35
>>> def add(x): return lambda y: x + y>>> y = add(2)>>> y(3)5
Therefore, to be more specific, f x y z
in Haskell is more
like f(x)(y)(z)
in Python.
We can also load Haskell source files into GHCI. Python source files
have the .py
extension; Haskell source files instead have the .hs
extension. Let us try writing a simple Haskell program. Create a new
file like MyCode.hs
and write in the following:
main :: IO () -- entry point to the programmain = putStrLn "Hello World!"
We will look at what the first line means in the future. For now, try
compiling and running your code by issuing the following commands in
your terminal (windows users might have to run ./MyCode.exe
):
~ $ ghc MyCode.hs~ $ ./MyCodeHello World!
The first command invokes GHC to compile your source file.
Compilation translates your source file into an executable file that
your computer that understand. The compilation process will also perform
a bunch of compile-time checks, such as type-checking etc. It may also
perform some optimizations. The outcome of invoking that command is an
executable (probably called MyCode
) along with other files (which we
shall not talk about for now). The second command then executes that
executable, and you should see Hello World!
shown in the terminal.
Everything is Immutable
We shall ignore compiling source files for now and temporarily focus on
working with GHCI. In GHCI, we can load files by issuing :l MyFile.hs
,
which loads the source into the shell. For now, write the following code
in MyCode.hs
:
z = 1 -- oky = 2 -- oky = 3 -- not ok!
As we have described earlier, everything in Haskell is immutable.
Therefore, re-defining what y
is should be disallowed! Let’s try
loading MyCode.hs
into GHCI:
~ $ ghciGHCi, version 9.4.8: https://www.haskell.org/ghc/ :? for helpghci> :l MyCode.hs[1 of 2] Compiling Main ( MyCode.hs, interpreted )MyCode.hs:3:1: error: Multiple declarations of 'y' Declared at: MyCode.hs:2:1 MyCode.hs:3:1 |3 | y = 3 -- not ok! | ^
As you can see, you cannot redefine functions or variables. Everything
is immutable in Haskell! Therefore, the statement x = e
is not
an assignment statement. Rather, it is a bind or a definition.
Control Structures
In Haskell, you mainly write expressions, and not statements.
Consequently, there are only if
-else
expressions, and no if
-else
statements. That means that you cannot omit an else
branch of an
if
-else
expression, just like in Python:
ghci> x = 2 * (-1)ghci> y = if x == 2 then "positive" else "negative"ghci> y"negative"
>>> x = 2 * -1>>> y = 'positive' if x == 2 else 'negative'>>> y'negative'
Just like in Python, if
-then
-else
expressions in Haskell are expressions and therefore
evaluate to a term:
ghci> (if 1 /= 2 then 3 else 4) + 58
>>> (3 if 1 != 2 else 4) + 58
Type of Conditionals
Importantly, the type of any expression is fixed, or at least, we should be able to determine what the type of every expression is unambiguously just by looking at it. Therefore, writing the following expression in Haskell will throw an error:
ghci> x = 2 * (-1)ghci> y = if x == 2 then 2 else "negative"<interactive>:2:20: error: - No instance for (Num String) arising from the literal '2' - In the expression: 2 In the expression: if x == 2 then 2 else "negative" In an equation for 'y': y = if x == 2 then 2 else "negative"
The reason is that we should not need to evaluate the truth of
x == 2
to determine what the type of the entire if
-else
expression is. Thus, Haskell requires that the type of the expression in
the if
branch be the same as the type of the expression in the else
branch. This departs from Python which is dynamically typed, where
types are determined at runtime, so expressions can freely be of
different types based on the values they inherit at the time of program
execution.
Functions
Defining functions in Haskell looks like defining a variable. This should be expected since Haskell is centred around functions, so it should come as no surprise that functions do not need to be defined with any special syntax.
ghci> oddOrEven x = if even x then "even" else "odd"ghci> oddOrEven 1"odd"ghci> oddOrEven 2"even"
ghci> quadratic c2 c1 c0 x = c2 * x ^ 2 + c1 * x + c0ghci> f = quadratic 1 2 3 -- x^2 + 2x + 3ghci> f 427ghci> f 538
>>> def odd_or_even(x):... if x % 2 == 0:... return 'even'... return 'odd'>>> odd_or_even(1)'odd'>>> odd_or_even(2)'even'
>>> def quadratic(c2, c1, c0):... return lambda x: c2 * x ** 2 + c1 * x + c0>>> f = quadratic(1, 2, 3) # x^2 + 2x + 3>>> f(4)27>>> f(5)38
Recursion
We might then ask: how do we write a loop in Haskell? Like we said earlier, Haskell is a purely functional programming language, so there are no loops (we may later see loops being simulated with functions). Thus, for now we shall use recursion as it is often the most elegant way to solve problems.
Recall that the familiar factorial
function may be written imperatively
in Python as:
def fac(n): res = 1 for i in range(2, n + 1): res *= i return res
As we know, the factorial function can be defined recursively as such:
Therefore, we can define factorial recursively in Haskell and Python like so:
ghci> fac n = if n == 0 then 1 else n * fac (n - 1)ghci> fac 424
>>> def fac(n):... return 1 if n == 0 else \... n * fac(n - 1)>>> fac(4)24
Guards
In fact, we can also express functions like fac
elegantly in Haskell
with guards. Guards allow us to define expressions differently based
on a condition.
For example, we know that the Fibonacci function may be written like so:
And writing this function with regular if
-else
expressions might look like:
ghci> :{ghci| fib n = if n == 0 || n == 1ghci| then 1ghci| else fib (n - 1) + fib (n - 2)ghci| :}
However, it might look clearer to define it this way with guards (otherwise
is just defined as True
):
ghci> :{ghci| fib nghci| | n == 0 = 1ghci| | n == 1 = 1ghci| | otherwise = fib (n - 1) + fib (n - 2)ghci| :}ghci> fib 58
Even better, we can use pattern matching to define such functions much more easily. We will look at pattern matching in more detail in the future:
ghci> fib 0 = 1ghci> fib 1 = 1ghci> fib n = fib (n - 1) + fib (n - 2)ghci> fib 58
Auxiliary Bindings
Thus far we have defined functions as a single expression; this is akin to writing a lambda expression in Python. As we know, that may not always be the most ergonomic considering that many functions can be better defined with several ‘statements’ that lead into a final expression. One example would be the following in Python:
def weight_sum(n1, w1, n2, w2): x = n1 * w1 y = n2 * w2 return x + y
While it is completely acceptable to define this function in one line,
it is not as readable. In Haskell, functions indeed have to be written
as a single expression, but we can define local bindings for the
expression using let
:
ghci> :{ghci| weightSum n1 w1 n2 w2 =ghci| let x = n1 * w1ghci| y = n2 * w2ghci| in x + yghci| :}ghci> weightSum 2 3 4 526
The let
binding allows us to introduce the definitions of
x
and y
which are used in the expression after
the in
clause. These make writing larger expressions more readable.
let
bindings are (more-or-less) syntactic sugar for function calls:
weightSum n1 w1 n2 w2 = let x = n1 * w1 y = n2 * w2 in x + y
-- same as
weightSum n1 w1 n2 w2 = f (n1 * w1) (n2 * w2)
f x y = x + y
Importantly, let
bindings are expressions; they therefore evaluate to
a value, as seen in this example:
ghci> (let x = 1 + 2 in x * 3) + 413
This is different to where
bindings, which also allow us to write
auxiliary definitions that support the main definition:
weightSum n1 w1 n2 w2 = let x = n1 * w1 y = n2 * w2 in x + y
-- same as
weightSum n1 w1 n2 w2 = x + y where x = n1 * w1 y = n2 * w2
Other differences between let
and where
are not so apparent at this
stage. You are free to use either appropriately (use let
where an
expression is desired, using either let
or where
are both okay in
other scenarios).
Data Types
We have looked at some simple data types so far: numbers like
1.2
, and strings like "abc"
. Strings are
actually lists of characters! Strings are surrounded by double
quotes, and characters are surrounded by single quotes, like
'a'
.
Lists
Lists in Haskell are singly linked list with homogenous data. That
means that the types of the elements in the list must be the same. We
can write lists using very familiar syntax, e.g. [1, 2, 3]
being a list containing the numbers 1, 2 and 3. Indexing a list can be
done with the !!
function.
ghci> x = [1, 2, 3]ghci> x !! 12
>>> x = [1, 2, 3]>>> x[1]2
Ranges
We can also construct ranges of numbers, or any enumerable type (such as characters). The syntax for creating such lists is straightforward as shown in the examples below.
ghci> y = [1,3..7]ghci> y[1,3,5,7]ghci> z = [1..10]ghci> z[1,2,3,4,5,6,7,8,9,10]ghci> inflist = [1..] -- 1,2,3,...ghci> inflist !! 1011
>>> y = list(range(1, 8, 2))>>> y[1, 3, 5, 7]>>> z = list(range(1, 11))>>> z[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
As we stated earlier, strings are lists of characters, we can even build ranges of characters which result in strings.
ghci> ['h', 'e', 'l', 'l', 'o']"hello"ghci> ['a'..'e']"abcde"ghci> ['a'..'e'] ++ ['A'..'D'] -- ++ is concatentation"abcdeABCD"
Head and Tail
As you know, a singly linked list is one of two things: an empty list,
or a node with a value (head
) and a reference to the remaining part of
the list (tail
). Thus, one of the most frequently used operations is the
cons operation (:
) which builds (or de-structures) a list
given its head and tail values. The :
operator is
right-associative.
ghci> x = [1, 2, 3]ghci> 0 : x[0,1,2,3]ghci> 0 : 1 : 2 : 3 : [][0,1,2,3]ghci> 'a' : "bcde""abcde"
One of the most interesting parts of Haskell is that it has non-strict evaluation. That means that the compiler is free to evaluate any expression only when it is needed. This allows us to quite nicely define recursive data without running into infinite loops:
ghci> y = 1 : yghci> take 5 y[1,1,1,1,1]
As we know, performing recursion over a list frequently requires us to
get a head element and then recursively calling the function over the
remaining list. This is nicely supported without any performance costs
unlike in Python, where ls[1:]
runs in
def sum(ls): if len(ls) == 0: return 0 return ls[0] + sum(ls[1:])
Haskell is very similar (head
is a function that returns the
first element of a list, and tail
is a function that returns
the remainder of a list):
sum' ls = if length ls == 0 then 0 else head ls + sum' (tail ls)
List Comprehension
Both Haskell and Python support list comprehension. The syntax for doing so is rather straightforward:
ghci> x = [1, 2, 3]ghci> y = "abc"ghci> [(i, j) | i <- x, j <- y, odd i][(1,'a'),(1,'b'),(1,'c'),(3,'a'),(3,'b'),(3,'c')]
>>> x = [1, 2, 3]>>> y = 'abc'>>> [(i, j) for i in x for j in y if i % 2 == 1][(1, 'a'), (1, 'b'), (1, 'c'), (3, 'a'), (3, 'b'), (3, 'c')]
Tuples
At this junction it would be most appropriate to discuss tuples. Like Python, the fields of a tuple can be of different types. However, tuples in Haskell are not sequences. Tuples behave more like the product of several types, as is usually the case in many domains.
As such, there are not many operations we can do on tuples. One of the only special cases is pairs, which have functions to project each value:
ghci> fst (1,"abc")1ghci> snd (1,(2,[3,4,5]))(2,[3,4,5])ghci> snd (snd (1,(2,[3,4,5])))[3,4,5]
>>> (1, 'abc')[0]1>>> (1, (2, [3, 4, 5]))[1](2, [3, 4, 5])>>> (1, (2, [3, 4, 5]))[1][1][3, 4, 5]
Conclusion
This should suffice for now. Now is your turn to try the exercises to get you started on your functional programming journey! Note that many of the functions we have used are built-in to Haskell, as defined in Haskell’s Prelude library. You may want to refer to this library when doing the exercises. A large portion of the Prelude documentation may be unreadable at this point, however, rest assured that many of the concepts presented in the documentation will be covered in this course.