Road to Haskeller #3 - Lists & Tuples

Last Edited: 6/14/2024

The blog post introduces lists and tuples, which are all important concepts in Haskell.

Haskell & Lists

Lists

Lists, as you might know already, are an invaluable data structure that get used extensively across various programming languages, and Haskell is no exception. In Haskell, a list can only store one type.

[1,2,3,4,5]::[Integer]

There are two list constructors in Haskell:

  • Empty list: []
  • Pre-pended list: x:xs

Using these list constructors, we can create [1,2,3,4,5] programmatically like the following:

1:2:3:4:5:[]

This will create [] -> [5] -> [4,5] -> [3,4,5] -> [2,3,4,5] -> [1,2,3,4,5].

Functions for Lists

As we use lists extensively, Haskell has a set of useful predefined functions for lists.

import Data.List 
--- This is where the predifined functions live
--- When trying the functions below, you simply need to import the above
 
--- Get the first element
head :: [a] -> a
head [1,2,3,4,5] --- Output: 1
 
--- Remove the first element
tail :: [a] -> [a]
tail [1,2,3,4,5] --- Output: [2,3,4,5]
 
--- Get the length
length :: [a] -> Integer
length [1,2,3,4,5] --- Output: 5
 
--- Remove the last element
init :: [a] -> [a]
init [1,2,3,4,5] --- Output: [1,2,3,4]
 
--- Determine if list is empty
null :: [a] -> Bool
null [] --- Output: True
null [1,2,3,4,5] --- Output: False

Functions for List of Booleans

There are some situations where you need to perform boolean logic on a list of booleans (e.g., checking if all the conditions are met). In such situations, the following functions will be helpful.

--- AND operator
and :: [a] -> Bool
and [True, False, True] --- Output: False
 
--- OR operator
or :: [a] -> Bool
or [True, False, True] --- Output: True

Now that we know the basic functions for lists, let's see how we can perform operations on lists.

List Comprehensions

We can use the following syntax for list comprehensions.

[<gen> | <elem> <- <list>, ..., <guard> , ...]
--- <gen> : operation on element(s) if guard(s) is/are met
--- <elem>: each element of a list
--- <list>: list
--- <guard>: guard condition (if you are not sure, check out Road to Haskeller #2)
 
--- Examples
[2*x | x <- [1,2,3]] --- Output: [2,4,6]
[2*x | x <- [1,2,3], x > 1] --- Output: [4,6]
[(x,y) | x <- [1,2,3], y <- [a,b], x>1] 
--- Output: [(2, a), (2, b), (3, a), (3, b)]

In the above, we can use as many lists and guards as possible.

List Patterns

In the previous article of Road to Haskeller (#2), we discussed pattern matching. Using this concept, we can define functions on lists very concisely.

--- Take sum of all the elements
sum :: [a] -> a
sum [] = 0
sum (x:xs) = x + sum xs
--- we are decomposing list and recursively adding elements
 
-- Extract even numbers
even :: [a] -> [a]
even [] = []
even (x:xs) = 
    | mod x 2 == 0 = x:even(xs) --- mod calculates reminder
    | otherwise = even(xs) --- do not prepend odd numbers

Tuples

Tuples are also an essential data type in Haskell, which can be defined as follows:

(1, "Hello") :: (Integer, String)
(1, 2) :: (Integer, Integer)

Unlike lists, tuples can have one or more types inside.

Functions for Tuples

Like lists, there are pre-defined functions for tuples as well.

--- Get the first element
fst:: (a, b) -> a
fst (x, _) = x
fst (1,2) --- Output: 1
 
--- Get the last element
snd:: (a, b) -> b
snd (_, y) = y
snd (1,2) --- Output: 2

That's it for today. Make sure you understand these data types because they are super important!

Exercises

This is an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.

Resources