https://lilymonade.fr/blog/feed.xml

Learn Haskell Part 2: Syntax and base concepts

2025-07-11

Install via GHCUp

In order to play with Haskell, you first need to install it on your system. Follow this guide to install it. For windows users, I recommend installing it on a Linux VM using WSL2, because my examples are crafted for linux users, but do as you want as long as you can run ghci.

Try ghci

In the terminal, launch the ghci command. It is a REPL, you can write any Haskell expression and it will evaluate it and print the result. Try it out:

ghci> 1+1
2
ghci>

You can change the prompt to anything you want. For the rest of this tutorial, I'll change the prompt to λ>:

ghci> :set prompt "λ> "
λ>

Expressions

In Haskell, everything is an expression, even the most complex program is just one big expression. Try to evaluate some:

λ> 10 < 5
False
λ> "hello" ++ " world"
"hello world"
λ> ['h', 'e', 'l', 'l', 'o']
"hello"
λ> False || True
True
λ> "false" && True
<interactive>:2:1: error: [GHC-83865]
Couldn't match type ‘[Char]’ with ‘Bool      Expected: Bool
        Actual: String
In the first argument of(||), namely ‘"false"      In the expression: "false" || True
      In an equation for ‘it’: it = "false" || True

My my, what happened ? Why did ghci insult us ? Let's look at the error. Could not match type [Char] with Bool. This means Haskell expected a value of type Bool (a boolean) but got a value of type [Char]. It even tells us where: In the first argument of (||), namely "false". This means the value "false" needed to be of type Bool, but its type is (obviously) [Char] (or String, since they are synonyms).

Everything has a type

As we saw earlier, Haskell expressions have types. And values are never implicitly converted. If your value is of type String, it will remain a String, unless you transform it with a function.

To see the type of an expression, you can use the :t ghci command:

λ> :t "hello"
"hello" :: String
λ> :t 2*2 > 0
2*2 > 0 :: Bool
λ> :t ["i", "am", "lily"]
["i", "am", "lily"] :: [String] -- list of strings
λ> :t 'i'
'i' :: Char
λ> :t 2*2
2*2 :: Num a => a

Wait wait wait... What is this again ? The type of 2*2 seems to be overly complicated for a simple multiplication. Why is it not Int or Number ? It has to do with polymorphism. Don't worry about that for now, just know when a type start with a lowercase letter, it means it's like a generic.

Why am I showing this now and not in the polymorphism section ? Because if you are curious, you will try :t on things I did not show, and I don't want you to feel lost. Trying things on your own is good ! Go for it !

Use the :t command a lot, in languages with well designed type systems, knowing the type of things is often all you need.

Functions

As said earlier, functions in Haskell are like other objects. They are the base of programs. Programs are just a big composition of functions.

Let's play with some of them:

λ> length "wiwiwi" -- gives the length of a list
6
λ> length [1, 2, 3]
3
λ> not True
False
λ> negate 10
-10
λ> head "king"
'k'
λ> tail "cat"
"at"

If you come from other languages, you may be surprised, because we don't "call" functions with the usual function(parameter) syntax. Well, this is inherited from lambda calculus, applying a function is just writing the function and the parameter next to each other: (function parameter).

They have types too. The "function" type is sometimes also called "arrow" type, and is noted TypeOfInput -> TypeOfOutput.

λ> :t not
not :: Bool -> Bool
λ> :t lines
lines :: String -> [String] -- split over the space character

Of course, applying a function to a value gives another value, which type is its output type:

λ> :t not False
not False :: Bool
λ> :t lines "one\ntwo\nthree"
lines "one\ntwo\nthree" :: [String]
λ> lines "one\ntwo\nthree"
["one", "two", "three"]

Operators are functions too ! Look:

λ> :t (&&) -- to treat operators as normal objects, wrap them in parenthesis
(&&) :: Bool -> Bool -> Bool
λ> (&&) True False -- use && as a function instead of an infix operator
False

Hmm, there are two arrows. This is weird, usualy functions that take two inputs, in other languages, are typed like (TypeA, TypeB) -> TypeOutput... Well, actualy this has to do with something called Currying, and we will talk about it later. But for now, just assume we just define multiple-parameters functions like so: Type1 -> Type2 -> Type3 -> ... -> TypeOutput. So yeah, the AND boolean operator takes two booleans and returns a boolean.

To call a multi-param function, simply put its parameters one after the other:

λ> take 5 "hello world"
"hello"

Polymorphism

You may have seen that I only showed Bool, String, Char... They are concrete types. But in Haskell, you can define objects that can have multiple types at once !

Parametric polymorphism

Parametric polymorphism has many names. In languages like C# and Java, they are called generics. In C++ they are called template types (because C++ does nothing like others do). The simplest example is the id function:

λ> :t id
id :: a -> a
λ> id "something"
"something"
λ> id 10
10
λ> id True
True

id function takes an object and returns it unchanged. This is why its type is a -> a: It takes an object of any type, and returns and object of the same type (We say that a is a type variable). In fact, there are as many id functions as there are types, and the compiler just choses the right one depending on its input ! One object, multiple forms (hence the name polymorphism).

Polymorphic types

Types can be polymorphic too, and we already used a polymorphic type before: the list. This type is a bit special, because it has no name but a specific syntax: [a]. Yes, you have it, [a] is "a list of elements of type a". And a is a type parameter, it can be anything.

A [Char] and a [Bool] are different types, but there are functions that can work on any list. Like the function take, or the operator (++):

λ> :t take
take :: Int -> [a] -> [a]
λ> :t (++)
(++) :: [a] -> [a] -> [a]

take needs an integer (Int), and a list of any type ([a]) and returns the firsts n elements of the list, so also a [a]. (++) takes two lists and chain them, so its type is pretty self-explanatory.

List is not the only polymorphic type, as you may have thought. One other very popular polymorphic type is the tuple. Tuples are collections of values of different types. For example, a point in a 2D space can be represented as a tuple of 2 Float: (Float, Float). But you can also want to associate person names with their wealth: (String, Float).

λ> (-3, 12) -- coordinate of some 2d point
(-3, 12)
λ> ("Nicolas", 10000000) -- wealth of Nicolas
("Nicolas", 10000000)

Types tell a lot on what the program does

Functions on tuples are interesting:

λ> :t fst
fst :: (a, b) -> a

Here you can see that fst has 2 type variables: a and b, but it only returns an object ot type a. Can you guess, using only the type of this function, what it does ?

Show answer

Since we know nothing about the type a, we cannot produce one from nothing. This means we need to take it from somewhere, and the only object of type a that the function gets is the first component of the tuple.

We can deduce that fst returns the first component of the given tuple.

Let's try guessing what other functions do only by their type:

func1 :: [a] -> [b] -> [(a, b)]
func2 :: [(a, b)] -> ([a], [b])
func3 :: [[a]] -> [a]
func4 :: (a, b) -> (b, a)
Show answer

  • func1 takes two lists, and pairs their elements one by one, creating a list of pairs. Its name is zip.
  • func2 takes a list of pairs, and separate the pairs, returning two lists, one of as and one of bs. Its name is unzip.
  • func3 takes a list of lists, and chain all the lists together one after the other. Its name is concat. You may not have the same type if you check it on ghci using :t, but it does the same (it's just more powerful in reality).
  • func4 takes a tuple and swaps its elements. It is the function swap.

As you can see, types encode a lot of information. One can manage to know what a function will do only by looking at its type ! Well, it's not always true, for example && and || are both of type Bool -> Bool -> Bool, but you get the idea. It is so powerful that if you search for a function in a package or the standard library you can search by type using Hoogle search engine. Try it, search for, let's say, a function of type a -> [a]. You should get two useful functions: one that creates an infinite list of the same object, and one that creates a list with a unique element.

Ad hoc polymorphism

capitaine ad hoc

Remember the type of 2*2 ? It was a bit complicated: Num a => a. Haskell type system has something called type classes. If you know some object oriented languages, you can see them as "interfaces". They are a bit more powerful than interfaces but for now it is a good analogy.

What ghci tells us here, is that 2*2 can be any type that can act like a number. The Num is the name of the type class, and it is the "type class of all numbers".

To know more about a specific type class, use the :i command:

λ> :i Num
type Num :: * -> Constraint
class Num a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
  {-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
        -- Defined in ‘GHC.Num’
instance Num Double -- Defined in ‘GHC.Float’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Word -- Defined in ‘GHC.Num’

It tells us that Num defines multiple objects:

  • operators: (*) (+) and (-)
  • functions: negate, abs, signum, fromInteger

:i even showed you some types that are Num (Double, Int, ...) and they look like number types to me so it makes sense ><

There are many more type classes, here is a non-exhaustive list:

NameObjectsDescription
Eq(==) (/=)Types that can be tested for equality
Ord(>) (<) (>=) (<=) compareTypes that can be ordered
Enumsucc predTypes that have a concept of successor and predecessor
BoundedminBound maxBoundTypes that have lower and upper limits
ShowshowTypes that can be printed as text
ReadreadTypes that can be parsed from text

Resolve ambiguate types using type annotation

Sometimes, an expression is not explicit enough, and Haskell cannot deduce its type. Let's say you wanted to parse an integer value and print it right after. You may write something like:

λ> show (read "123")
"*** Exception: Prelude.read: no parse

But it gives you a runtime error. This is because the read function has type Read a => String -> a, meaning it can output any type that can be parsed, and the function show has type Show a => a -> String, meaning it can take as input any type that can be printed. Since there are multiple type that can do both, ghci cannot know which type to chose, and it chose wrong. We wanted an Int but we got a () (more on this type later).

λ> show (read "()")
"()"

To avoid errors like this, you can force the type of an expression by annotating it using the :: Type syntax. Here:

λ> show (read "123" :: Int)
"123"

Now it works, because we forced the type of read "123" to be Int, so Haskell knew it had to select the read :: String -> Int version, and not another version of read.

Bindings

When you want to re-use an expression (most of the time it's because this expression is a function), you can give it a name. It won't evaluate it, but it will allow you to use it multiple times without re-writing it every time.

λ> x = 2
λ> y = x+x
λ> z = y*y
λ> z
16

Be careful, bindings are not variables. We call them often variables, but they are only names given to expressions. Because of that, you can have weird behaviors in Haskell with code that seems it should work in other languages:

λ> a = a + 1
λ> a
-- and your ghci hangs (do CTRL+C to stop the last evaluation)

Why did it do this ? Well, because I gave the name a to the expression a + 1, and tried to evaluate a. Remember the exercises on how to reduce lambda calculus expressions ? Well Haskell works exactly like this. Why don't you try to reduce a ?

Show the reduction

Haskell entered an infinite loop, it will never reduce to a value !

Bindings as expressions

When you write name = expression in a file, it creates a global binding. If you only want this binding to appear in an expression, you can use let expressions. Here are some examples:

λ> let x = 2 in x+x
4
λ> x

<interactive>:2:1: error: [GHC-88464] Variable not in scope: x

x only exists in the first expression !

Modules

Programs and libraries in Haskell are written in files ending in .hs. These files are called Haskell modules. You can define multiple bindings in a module, and loading it will give you access to these bindings. Try writing some bindings in a file let's say Bindings.hs:

one = 1
two = one + 1
myname = "lily"

And load the module in ghci using command :l Bindings.hs. Now play a bit with this module:

λ> one
1
λ> myname
"lily"

You can filter what you want to expose from the module, but for now we will just expose everything.

Importing a module

Importing a module is done using the keyword import and naming the module to import.

-- import functions relative to Char type
import Data.Char

You can also import modules in ghci:

λ> import Data.Char
λ> :t ord
ord :: Char -> Int
λ> ord '0'
48

Functions are expressions too

For now we only saw how to use functions. But we also need to know how to create functons. The syntax is very simple. \parameter1 parameter2 ... -> expression.

λ> inc = \x -> x+1
λ> inc 10
11
λ> hyp = \x y -> sqrt (x*x + y*y)
λ> hyp 3 4
5.0

It looks almost the same as lambda abstractions in lambda calculus, the \ and -> replace the λ and . . Functions are exactly like values. The only problem in ghci is that we cannot show them. So if you want to create functions, always give them a name.

Functions are normal expressions, so you can also use them in let expressions:

λ> hyp = \x y -> let square = \x -> x*x in sqrt (square x + square y)
λ> hyp 6 8
10.0

Ok, here it is useless, but we will see why it is useful later with recursion.

Function syntactic sugar

It can be tiring to always write functions like this. So creating functions and giving them a name, we have syntactic sugar. It is "a syntax that is translated to another syntax" just to help writing things. No new semantic, just code rewrite at compilation.

λ> inc x = x+1
λ> inc 10
11
λ> hyp x y = let square x = x*x in sqrt (square x + square y)
λ> hyp 3 4
5.0

Conditions, and pattern matching

Until now, we never made choices. Always only one path to take, one expression to reduce to. But some functions need multiple choices. For example, the Fibonacci sequence is defined differently given its rank.

The sequence has the same expression for all , but for or , the expression changes.

Conditions

Conditions are branches based on boolean expression this means you need a boolean expression (i.e. a comparison) and at runtime, it will chose a path based on the value (True or False) of this expression.

If then else

λ> if True then "yes" else "no"
"yes"
λ> if not True then "yes" else "no"
"no"
λ> syracuse n = if even n then div n 2 else 3*n + 1
λ> syracuse 3
10
λ> syracuse 10
5

if then else is an expression, so it has a type. Let's define a function that just "applies" an if then else and see which types does Haskell compute:

λ> ite condition yes no = if condition then yes else no
λ> :t ite
ite :: Bool -> a -> a -> a

This type is really interesting. First, we see that condition must indeed be a Bool expression. But then we see a type variable. So the then and else branches can take expressions of any type BUT there is a catch. We see that there is only one type variable, so both branches need to contain expressions of the same type. That makes sense, if it is an expression it must have only one type at runtime (even if this type can vary depending on the context at compile time). What would happen if, let's say you could define this function:

λ> intorstring c = if c then 10 else "yo"

and then do something like:

λ> (intorstring didIDoTheDishes) + 1

At runtime, the program would not work half of the time, because if you did not do the dishes before running the program, it would output a String, and Strings are not numbers. Now you start to understand why stricly typed programs are easier to work with once you understand types. No more weird bugs like in JavaScript or Python ><

Guards

Sometimes, a function can have a lot of different cases. We could write a tree of if then else (if then else (if then else (...))), but it would be really difficult to read really quicly. So Haskell has syntactic sugar (yes, again) to help us. Let us say you want to give a description of an earthquake based on its score in the Richter scale. Using if then else, it would be written as:

richterClass n = if n < 2 then "micro" else if n < 3 then "minor" else if n < 4 then "slight"
  else if n < 5 then "light" else if n < 6 then "moderate" else if n < 7 then "strong" else if n < 8
  then "major" else if n < 9 then "great" else "extreme"

It is absolutely unreadable ! Now let's see how it would work using guards:

richterClass n
  | n < 2 = "micro"
  | n < 3 = "minor"
  | n < 4 = "slight"
  | n < 5 = "light"
  | n < 6 = "moderate"
  | n < 7 = "strong"
  | n < 8 = "great"
  | n < 9 = "major"
  | otherwise = "extreme"

This is much more readable, and can also be extended way more easily. This is syntactic sugar because if just translates to a cascade of if then else like the one we saw earlier so that it tries the first branch, and try the next ones in order until one succeeds. The last condition is otherwise. which translates to just True. You can even see this using ghci:

λ> otherwise
True

So the last branch translates to if True then "extreme" else error "non exhaustive patterns ...". You can see that it does translate to this by creating a function without a "catch all" condition.

bad n
  | n < 10 = "less than ten"

Now let's call bad:

λ> bad 9
"less than ten"
λ> bad 42
*** Exception: <interactive>:(18,1)-(19,16): Non-exhaustive patterns in function bad

bad is called a partial function. We try to never create partial functions because they can crash. This is why I will not talk in details about functions like error. We will see how to use the type system to handle errors later.

Pattern matching

We saw that conditions work on any boolean expression. They are great, but the compiler cannot reason about them. For example, let's say we want to take the head of a list OR zero if it is empty.

λ> headOrZero l = if null l then 0 else head l

This function works perfectly fine, because of course if l is not null then head l will never crash. BUT, the condition you put in your if can be absolutely anything, and the compiler will never complain if the expression has the right type. Let's say you created a function nul :: [a] -> Bool but it checks another condition. If you do a typo and write nul instead of null in the definition of headOrZero, the program will compile perfectly but it may crash now because we may get head l when l is empty.

Of course it would absolutely never happen, right ? You are a good programmer, you know how to write code, and you even have linters to help you... right ? Wrong. This happens all the time. We are human, we make mistakes, even in things we do all days. I know how to walk and avoid stuff in my vision, but sometimes my toe hits the chair. And I cry. And I hate myself... Anyway, don't rely on conditions only. Sometimes we can do better. And this better is pattern matching.

In Haskell, some types have a "shape". For example, tuples have the shape (firstVal, secondVal), lists can be either [] (an empty list), or composed of a head attached to a tail: head:tail. And you can tell Haskell what to do depending on the shape or a value.

case of

The first kind of pattern matching we will see is the case of expression.

case expr of
  pattern1 -> expression1
  pattern2 -> expression2
  -- ...

For example, if we want to implement the null function, we can write:

null l = case l of
  [] -> True
  h:t -> False

We match against the shape []. If the list has this shape, it is empty, so null l is True. If it has the h:t shape (a head followed by a tail), well, it is not empty since it has at least one element (the head).

Pattern matching can also be used to destruct values (access their parts). For example, the partial head function can be written:

head l = case l of h:t -> h

This function is partial because it has no branch handling the [] shape. And Haskell can warn us. If it did not warn you, you can enable the warning using this ghci command: :set -fwarn-incomplete-patterns

λ> :set -fwarn-incomplete-patterns
λ> head l = case l of h:t -> h

<interactive>:34:10: warning: [GHC-62161] [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In a case alternative: Patterns of type ‘[a]’ not matched: []

Another win for Haskell's type system.

We can also use pattern matching on tuples, let's implement the functions fst and snd which respectively get the first and second component of a tuple:

fst t = case t of (f, s) -> f
snd t = case t of (f, s) -> s

Now try to implement the same functions but for tuples of 3 elements:

fst3 :: (a, b, c) -> a
snd3 :: (a, b, c) -> b
thd3 :: (a, b, c) -> c
Show solution

fst3 t = case t of (a, b, c) -> a 
snd3 t = case t of (a, b, c) -> b
thd3 t = case t of (a, b, c) -> c 

You see that you don't have any warning for these functions. This is because the pattern used catches all possible shapes of a tuple. The case of is exhaustive, our function is total.

Variable patterns

When you see a name in a pattern, the name itself is a pattern. This pattern is like saying "whatever shape is at this place, I don't care I just want to bind the value to a name". So saying:

case l of
  h:t -> h

Means: "what shape has h is irrelevant, I just need it to be at the head of l, and I name it h because I need to use it later.". So you can have a pattern with only a name, and it will catch everything. See this new version of null:

null l = case l of
  [] -> True
  blerp -> False

We don't care about the shape of the list if it is not [], so we can just put a variable there.

Number patterns

Numbers also have shapes, 0, 1, 2048... so you can pattern match against numbers too:

iszero n = case n of
  0 -> True
  n -> False

You see that ? It's almost like the Fibonacci sequence, we define our expression when n is something. Let's translate the mathematical definition in Haskell:

f n = case n of
  0 -> 0
  1 -> 1
  n -> f (n-1) + f (n-2)

Multiple definition

When a function contains a single case of expression, it can be easier to write it using multile definitions. You simply define the function multiple times with a different pattern each time. Yes, in fact, function parameters are patterns !

f 0 = 0
f 1 = 1
f n = f (n-1) + f (n-2)

Try it, write these lines in a file and load it in ghci using :l TheFile.hs. You can even write them in a let expression:

g n = let f 0 = 0; f 1 = 1; f n = f(n-1)+f(n-2) in f (2*n)

We created a function that computes Fibonacci(n*2) by defining the fibonacci function inside g's expression.

Indentation rules

Before we continue, we need to talk a bit about expression delimitation. You can do whatever you want with indentations as long as you follow 2 rules:

  • An expression MUST be at least one column further than the start of its definition block (blocks of the form pattern = expression or pattern -> expression.
  • Items that are part of the same item sequence MUST start at the same column. Item sequences are things like let def1 = ... def2 = ... in definitions, case ... of pat1 -> ... pat2 -> ..., items defined in where clauses, and a last thing: instructions in do blocks. (But we will see this last one much later).
-- this is totaly UNREADABLE but valid haskell
f n =
        let
    -- definitions of x and y start at same colum
    x
    -- every other line of the definition
    -- of x are more on the right
            = 
     n+1
    y = n-1
       in  -- keywords are part of an expression, they follow the expression rule
           -- the 'in' and 'case' keywords here belong to the top definition
           -- (the definition of function 'f')
      case
              -- n is only an expression it goes
              -- wherever it pleases
               n
  of
 -- 0 and n start on the same column
 -- 1 and 2 (the expressions associated with each definition)
 -- are futher right
 0 -> 
  1
 n -> 2
-- this is not valid haskell
fibonacci n = case n of
    0 ->
    0 -- the expression is not at least 1 column futher on the right
    1 -> 1
   -- this last definition is not on the same column as the 0 and 1 defs
     n ->
     -- the let and in are not indented enough since they belong to the
     -- expression associated with the pattern 'n ->'
     let
      f1 = fibonacci (n-1)
      f2 = fibonacci (n-1)
     in
      f1
        +
         f2
-- everything else is totaly fine, just 3 errors
-- only 2 if you count the fact that correcting the second error will correct the third

For the rest of this tutorial I will try to follow this syntactic rule:

  • If it has only one definition, and the definition fits on one line, don't indent
  • If it needs multiple lines, or it does not fit on one line, indent only what is needed by 2 spaces

For example:

function exp = case exp of
  pat1 -> exp1
  pat2 -> case exp2 of
    pat21 -> exp21
    pat22 ->
      a
      kind
      of
      long
      expression
  pat1 -> exp1

function2 param = let
  pat1 = case exp1 of
    pat11 -> exp11
    pat12 -> exp12
  pat2 = exp2
  pat3 = exp3
  in someExpr

Previous chapter Next chapter