Learning Haskell: Lists

posted 2 years ago

The List Datatype

A list is a data structure used to store multiple homogeneous values.

Lists are also used as an infinite series of values, usually generated by a function, which allows them to act as a stream datatype.

The list datatype is defined as:

data []  a  =  []   |  a : [a]
--  [1] [2][3] [4] [5]  [6]
  1. The datatype with the type constructor []
  2. takes a single type constructor argument a
  3. can be constructed by
  4. nullary constructor []
  5. or it can be constructed by
  6. data constructor which is a product of a value of the type a mentioned in the type constructor and a value of type [a]

The [] data constructor is a nullary constructor because it takes no arguments.

The second data constructor, has arguments. (:) is an infix operator usually called 'cons' which is short for constructs. Here, cons takes a value of type a and a list of type [a] and evaluates to [a].

The list datatype is a sum type because it is either an empty list or a single value with more list.

Pattern Matching on Lists

We can pattern match on data constructors for lists just like we do with data constructors.

We can match on the first argument to the infix (:) constructor:

Prelude> let myHead (x : _) = x -- returns first argument given
Prelude> :t myHead
myHead :: [t] -> t
Prelude> myHead [1, 2, 3]
1

We could rewrite this using Maybe which I will discuss more in future articles:

safeHead :: [a] -> Maybe [a]
safeHead [] = Nothing
safeHead ([]:x) = Nothing
safeHead (xs:_) = Just xs

Syntactic Sugar

Prelude> [1, 2, 3] ++ [4] -- write this
[1, 2, 3, 4]
Prelude> (1 : 2 : 3 : []) ++ 4 : [] -- not this
[1,2,3,4]

Using Ranges to Construct Lists

One of the most simple ways to construct lists is with ranges.

Prelude> [1..10]
[1,2,3,4,5,6,7,8,9,10]
Prelude> enumFromTo 1 10
[1,2,3,4,5,6,7,8,9,10]
Prelude> [1,2..10]
[1,2,3,4,5,6,7,8,9,10]
Prelude> enumFromThenTo 1 2 10
[1,2,3,4,5,6,7,8,9,10]
Prelude> [2,4..10]
[2,4,6,8,10]
Prelude> enumFromThenTo 2 4 10
[2,4,6,8,10]
Prelude> ['a'..'c']
"abc"

Extracting Portions of Lists

There are some useful functions for extracting portions of lists:

take :: Int -> [a] -> [a] -- takes the specified number of elements out of a list and returns a list
drop :: Int -> [a] -> [a] -- drops the specified number of elements off the beginning of the list
splitAt :: Int -> [a] -> ([a], [a]) -- cuts a list into two parts at the element specified byt the Int and makes a tuple
Prelude> take 3 [1..10]
[1,2,3]
Prelude> drop 5 [1..10]
[6,7,8,9,10]
Prelude> splitAt 5 [1..10]
([1,2,3,4,5],[6,7,8,9,10])

List Comprehensions

List comprehensions are a means of generating a new list from a list or lists.

They come directly from the concept of set comprehensions in mathematics.

Let's look at an example:

  [ x^2  |   x <- 1..10]
--  [1] [2] [    3     ] 
  1. The output function that will apply to the members of the list we indicate
  2. The pip here designates the separation between the output and input function
  3. The input set: a generator list and a variable that represents the elements that will be drawn from that list

Output:

Prelude> [x^2 | x <- [1..10]]
[1,4,9,16,25,36,49,64,81,100]

Adding Predicates

List comprehensions can optionally take predicates that limit the elements drawn from the generator list.

Predicates must evaluate to Bool values.

Let's say we wanted the same output as the other function but with only the squares of odd numbers:

Prelude> [x^2 | x <- [1..10], odd x]

Spines and Non-strict Evaluation

Lists are a recursive series of cons cells a : [a] terminated by the empty list [].

The spine of a data structure is the structure that ties the collection of values together.

In the case of a list, the spine is represented by the recursive cons (:) operators.

When a list in constructed, it starts from the bottom of the list up the spine, putting the first element in the empty list and so on.

A list isn't constructed until it is consumed.

Spines are Evaluated Independently of Values

Values in Haskell get reduced to weak head normal form by default.

Normal form means that the expression is fully evaluated.

Weak head normal form means that the expression is only evaluated as far as is necessary to reach a data constructor.

Example of both WHNF (weak head normal form) and NF (normal form):

(5, 7)

This example is in NF and is fully evaluated.

Anything that is in NF is also in WHNF because weak head is an expression which is evaluated up to at least the first data constructor.

NF exceeds that precedent by requiring that all subexpressions by fully evaluated.

(1, 2 + 5)

This example is in WHNF but not in NF.

This is because it is not fully evaluated.

Transforming Lists of Values

One common thing that you may want to do is return a list with a function applied uniformly to all values within the list.

We can either use the map or fmap functions for this purpose.

map can only be used with [] whereas fmap is defined in a typeclass called Functor and can be applied to data other than lists.

Prelude> map (+2) [1, 2, 3]
[3,4,5]
Prelude> fmap (+1) [1,2,10]
[2,3,11]
Prelude> map fst [(1, 1), (2, 2), (3, 3)]
[1,2,3]

Filtering Lists of Values

The filter function takes a function that returns a Bool value, maps that function over a list, and return a new list of all the values that met the condition.

Filtering builds a new list including values that meet the condition and excluding that ones that do not. It does not mutate the existing list.

Prelude> multiplesOf3Filter xs = filter $ \x -> x `mod` 3 == 0
Prelude> multiplesOf3Filter [1..30]
[3,6,9,12,15,18,21,24,27,30]

Prelude> removeArticlesFromSentence = filter (\x -> x `notElem` ["a", "the", "an"]) . words

Zipping Lists

Zipping lists together is a means of combining values from multiple lists into a single list.

zip, unzip, and zipWith are the important functions to know with zipping.

Prelude> zip [2, 3, 4] [1, 2, 3]
[(2, 1), (3, 2), (4, 3)]

Prelude> zip [2, 2] [1] -- the zip stops when one of the lists runs out of values
[(2, 1)]

Prelude> unzip [(2, 1)]
([2],[1])

Prelude> zipWith (*) [1, 2, 3] [10, 11, 12]
[10,22,36]

Sources

Data.List Documentation - http://hackage.haskell.org/package/base/docs/Data-List.html

Haskell Book - https://haskellbook.com/