Functional Programming

26 minute read

Functional Programming

Now with lists and polymorphism in our toolbox, we can finally start to look at functional programming.

In Haskell a function is a value, just like a number or a list is. Functions can be passed as parameters to other functions. Here’s a toy example. The function applyTo1 takes a function of type Int->Int, applies it to the number 1, and returns the result.

applyTo1 :: (Int -> Int) -> Int
applyTo1 f = f 1

Let’s define a simple function of type Int->Int and see applyTo1 in action.

addThree :: Int -> Int
addThree x = x + 3
applyTo1 addThree
  ==> addThree 1
  ==> 1 + 3
  ==> 4

Let’s go back to the type annotation for applyTo1.

applyTo1 :: (Int -> Int) -> Int

The parentheses are needed because the type Int -> Int -> Int would be the type of a function taking two Int arguments. More on this later.

Let’s look at a slightly more interesting example. This time we’ll implement a polymorphic function doTwice. Note how we can use it with various types of values and functions.

doTwice :: (a -> a) -> a -> a
doTwice f x = f (f x)
doTwice addThree 1
  ==> addThree (addThree 1)
  ==> 7
doTwice tail "abcd"
  ==> tail (tail "abcd")
  ==> "cd"
makeCool :: String -> String
makeCool str = "WOW " ++ str ++ "!"
doTwice makeCool "Haskell"
  ==> "WOW WOW Haskell!!"

Functional Programming on Lists

That was a bit boring. Luckily there are many useful list functions that take functions as arguments. By the way, functions that take functions as arguments (or return functions) are often called higher-order functions.

The most famous of these list-processing higher-order functions is map. It gives you a new list by applying the given function to all elements of a list.

map :: (a -> b) -> [a] -> [b]
map addThree [1,2,3]
  ==> [4,5,6]

The partner in crime for map is filter. Instead of transforming all elements of a list, filter drops some elements of a list and keeps others. In other words, filter selects the elements from a list that fulfill a condition.

filter :: (a -> Bool) -> [a] -> [a]

Here’s an example: selecting the positive elements from a list

positive :: Int -> Bool
positive x = x>0
filter positive [0,1,-1,3,-3]
  ==> [1,3]

Note how both the type signatures of map and filter use polymorphism. They work on all kinds of lists. The type of map even uses two type parameters! Here are some examples of type inference using map and filter.

onlyPositive xs = filter positive xs
mapBooleans f = map f [False,True]
Prelude> :t onlyPositive
onlyPositive :: [Int] -> [Int]
Prelude> :t mapBooleans
mapBooleans :: (Bool -> b) -> [b]
Prelude> :t mapBooleans not
mapBooleans not :: [Bool]

One more thing: remember how constructors were just functions? That means you can pass them as arguments to other functions!

wrapJust xs = map Just xs
Prelude> :t wrapJust
wrapJust :: [a] -> [Maybe a]
Prelude> wrapJust [1,2,3]
[Just 1,Just 2,Just 3]

Examples of Functional Programming on Lists

How many “palindrome numbers” are between 1 and n?

-- a predicate that checks if a string is a palindrome
palindrome :: String -> Bool
palindrome str = str == reverse str

-- palindromes n takes all numbers from 1 to n, converts
-- them to strings using show, and keeps only palindromes
palindromes :: Int -> [String]
palindromes n = filter palindrome (map show [1..n])
palindrome "1331" ==> True
palindromes 150 ==>
  ["1","2","3","4","5","6","7","8","9",
   "11","22","33","44","55","66","77","88","99",
   "101","111","121","131","141"]
length (palindromes 9999) ==> 198

How many words in a string start with “a”? This uses the function words from the module Data.List that splits a string into words.

countAWords :: String -> Int
countAWords str = length (filter startsWithA (words str))
  where startsWithA s = head s == 'a'
countAWords "does anyone want an apple?"
  ==> 3

The function tails from Data.List returns the list of all suffixes (“tails”) of a list. We can use tails for many string processing tasks. Here’s how tails works:

tails "echo"
  ==> ["echo","cho","ho","o",""]

Here’s an example where we find what characters come after a given character in a string. First of all, we use tails, map and take to get all substrings of a certain length:

substringsOfLength :: Int -> String -> [String]
substringsOfLength n string = map shorten (tails string)
  where shorten s = take n s
substringsOfLength 3 "hello"
  ==> ["hel","ell","llo","lo","o",""]

There’s some shorter substrings left at the end (can you see why?), but they’re fine for our purposes right now. Now that we have substringsOfLength, we can implement the function whatFollows c k s that finds all the occurrences of the character c in the string s, and outputs the k letters that come after these occurrences.

whatFollows :: Char -> Int -> String -> [String]
whatFollows c k string =
  map tail (filter match (substringsOfLength (k+1) string))
  where match sub = take 1 sub == [c]
whatFollows 'a' 2 "abracadabra"
  ==> ["br","ca","da","br",""]

Partial Application

When using higher-order functions you can find yourself defining lots of small helper functions, like addThree or shorten in the previous examples. This is a bit of a chore in the long run, but luckily Haskell’s functions behave a bit weirdly…

Let’s start in GHCi:

Prelude> let add a b = a+b
Prelude> add 1 5
6
Prelude> let addThree = add 3
Prelude> addThree 2
5

So, we’ve defined add, a function of two arguments, and only given it one argument. The result is not a type error but a new function. The new function just stores (or remembers) the given argument, waits for another argument, and then gives both to add.

Prelude> map addThree [1,2,3]
[4,5,6]
Prelude> map (add 3) [1,2,3]
[4,5,6]

Here we can see that we don’t even need to give a name to the function returned by add 3. We can just use it anywhere where a function of one argument is expected.

This is called partial application. All functions in Haskell behave like this. Let’s have a closer look. Here’s a function that takes many arguments.

between :: Integer -> Integer -> Integer -> Bool
between lo high x = x < high && x > lo
Prelude> between 3 7 5
True
Prelude> between 3 6 8
False

We can give between less arguments and get back new functions, just like we saw with add:

Prelude> (between 1 5) 2
True
Prelude> let f = between 1 5 in f 2
True
Prelude> map (between 1 3) [1,2,3]
[False,True,False]

Look at the types of partially applying between. They behave neatly, with arguments disappearing one by one from the type as values are added to the expression.

Prelude> :t between
between :: Integer -> Integer -> Integer -> Bool
Prelude> :t between 1
between 1 :: Integer -> Integer -> Bool
Prelude> :t between 1 2
between 1 2 :: Integer -> Bool
Prelude> :t between 1 2 3
between 1 2 3 :: Bool

Actually, when we write a type like Integer -> Integer -> Integer -> Bool, it means Integer -> (Integer -> (Integer -> Bool)). That is, a multi-argument function is just a function that returns a function. Similarly, an expression like between 1 2 3 is the same as ((between 1) 2) 3, so passing multiple arguments to a function happens via multiple single-argument calls. Representing multi-argument functions like this is called currying (after the logician Haskell Curry). Currying is what makes partial application possible.

Here’s another example of using partial application with map:

map (drop 1) ["Hello","World!"]
  ==> ["ello","orld!"]

In addition to normal functions, partial application also works with operators. With operators you can choose whether you apply the left or the right argument. (Partially applied operators are also called sections or operator sections). Some examples:

Prelude> map (*2) [1,2,3]
[2,4,6]
Prelude> map (2*) [1,2,3]
[2,4,6]
Prelude> map (1/) [1,2,3,4,5]
[1.0,0.5,0.3333333333333333,0.25,0.2]

Prefix and Infix Notations

Normal Haskell operators are applied with prefix notation, which is just a fancy way to say that the function name comes before the arguments. In contrast, operators are applied with infix notation – the name of the function comes between the arguments.

An infix operator can be converted into a prefix function by adding parentheses around it. For instance,

(+) 1 2 ==> 1 + 2 ==> 3

This is useful especially when an operator needs to be passed as an argument to another function.

As an example, the function zipWith takes two lists, a binary function, and joins the lists using the function. We can use zipWith (+) to sum two lists, element-by-element:

Prelude> :t zipWith
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
Prelude> zipWith (+) [0,2,5] [1,3,3]
[1,5,8]

Without the ability to turn an operator into a function, we’d have to use a helper function – such as add above.

Note that omitting the parentheses leads into a type error:

Prelude> zipWith + [0,2,5,3] [1,3,3]

<interactive>:1:11: error:
  • Couldn't match expected type
      ‘[Integer] -> (a -> b -> c) -> [a] -> [b] -> [c]’
                with actual type ‘[Integer]’
  • The function ‘[0, 2, 5, 3]’ is applied to one argument,
    but its type ‘[Integer]’ has none
    In the second argument of ‘(+)’,
      namely ‘[0, 2, 5, 3] [1, 3, 3]’
    In the expression: zipWith + [0, 2, 5, 3] [1, 3, 3]
  • Relevant bindings include
      it :: (a -> b -> c) -> [a] -> [b] -> [c]
        (bound at <interactive>:1:1)

The reason for this weird-looking error is that GHCi got confused and thought that we were somehow trying to add zipWith and [0,2,5,3] [1,3,3] together. Logically, it deduced that [0,2,5,3] must be a function since it’s being applied to [1,3,3] (remember that functions bind tighter than operators).

Unfortunately, error messages can sometimes be obscure, since the compiler cannot always know the “real” cause of the error (which is in this case was omitting the parentheses). Weird error messages are frustrating, but only the programmer knows what was the original intent behind the code.

Another nice feature of Haskell is the syntax for applying a binary function as if it was an infix operator, by surrounding it with backticks (`). For example:

6 `div` 2 ==> div 6 2 ==> 3
(+1) `map` [1,2,3] ==> map (+1) [1,2,3] ==> [2,3,4]

Lambdas

The last spanner we need in our functional programming toolbox is λ (lambda). Lambda expressions are anonymous functions. Consider a situation where you need a function only once, for example in an expression like

let big x = x>7 in filter big [1,10,100]

A lambda expression allows us to write this directly, without defining a name (big) for the helper function:

filter (\x -> x>7) [1,10,100]

Here are some more examples in GHCi:

Prelude> (\x -> x*x) 3
9
Prelude> (\x -> reverse x == x) "ABBA"
True
Prelude> filter (\x -> reverse x == x) ["ABBA","ACDC","otto","lothar","anna"]
["ABBA","otto","anna"]
Prelude> (\x y -> x^2+y^2) 2 3           -- multiple arguments
13

The Haskell syntax for lambdas is a bit surprising. The backslash character (\) stands for the greek letter lambda (λ). The Haskell expression \x -> x+1 is trying to mimic the mathematical notation λx. x+1. Other languages use syntax like x => x+1 (JavaScript) or lambda x: x+1 (Python).

Note! You never need to use a lambda expression. You can always instead define the function normally using let or where.

By the way, lambda expressions are quite powerful constructs which have a deep theory of their own, known as Lambda calculus. Some even consider purely functional programming languages such as Haskell to be typed extensions of Lambda calculus with extra syntax.

Sidenote: The . and $ Operators

The two most common operators in Haskell codebases are probably . and $. They are useful when writing code that uses higher-order functions. The first of these, the . operator, is the function composition operator. Here’s its type

(.) :: (b -> c) -> (a -> b) -> a -> c

And here’s what it does

(f.g) x ==> f (g x)

You can use function composition to build functions out of other functions, without mentioning any arguments. For example:

double x = 2*x
quadruple = double . double  -- computes 2*(2*x) == 4*x
f = quadruple . (+1)         -- computes 4*(x+1)
g = (+1) . quadruple         -- computes 4*x+1
third = head . tail . tail   -- fetches the third element
                             --   of a list

We can also reimplement doTwice using (.). Note how we can use doTwice both as applied only to a function, or as applied to a function and a value.

doTwice :: (a -> a) -> a -> a
doTwice f = f . f
let ttail = doTwice tail
in ttail [1,2,3,4]
  ==> [3,4]

(doTwice tail) [1,2,3,4] ==> [3,4]

doTwice tail [1,2,3,4] ==> [3,4]

Often function composition is not used when defining a new function, but instead to avoid defining a helper function. For instance, consider the difference between these two expressions:

let notEmpty x = not (null x)
in filter notEmpty [[1,2,3],[],[4]]
  ==> [[1,2,3],[4]]
filter (not . null) [[1,2,3],[],[4]]
  ==> [[1,2,3],[4]]

The other operator, $ is more subtle. Let’s look at its type.

($) :: (a -> b) -> a -> b

It takes a function of type a -> b and a value of type a, and returns a value of type b. In other words, it’s a function application operator. The expression f $ x is the same as f x. This seems pretty useless, but it means that the $ operator can be used to eliminate parentheses! These expressions are the same:

head (reverse "abcd")
head $ reverse "abcd"

This isn’t that impressive when it’s used to eliminate one pair of parentheses, but together . and $ can eliminate lots of them! For example, we can rewrite

reverse (map head (map reverse (["Haskell","pro"] ++ ["dodo","lyric"])))

as

(reverse . map head . map reverse) (["Haskell","pro"] ++ ["dodo","lyric"])

and then

reverse . map head . map reverse $ ["Haskell","pro"] ++ ["dodo","lyric"]

Sometimes the operators . and $ are useful as functions in their own right. For example, a list of functions can be applied to an argument using map and a section of $:

map ($"string") [reverse, take 2, drop 2]
  ==> [reverse $ "string", take 2 $ "string",
       drop 2 $ "string"]
  ==> [reverse "string", take 2 "string", drop 2 "string"]
  ==> ["gnirts", "st", "ring"]

If this seems complicated, don’t worry. You don’t need to use . and $ in your own code until you’re comfortable with them. However, you’ll bump into . and $ when reading Haskell examples and code on the internet, so it’s good to know about them. This article might also help.

Example: Rewriting whatFollows

Now, let’s rewrite the whatFollows example from earlier using the tools we just saw. Here’s the original version:

substringsOfLength :: Int -> String -> [String]
substringsOfLength n str = map shorten (tails str)
  where shorten s = take n s

whatFollows :: Char -> Int -> String -> [String]
whatFollows c k str =
  map tail (filter match (substringsOfLength (k+1) str))
  where match sub = take 1 sub == [c]

To get started, let’s get rid of the helper function substringsOfLength and move all the code to whatFollows:

whatFollows c k str =
  map tail (filter match (map shorten (tails str)))
  where shorten s = take (k+1) s
        match sub = take 1 sub == [c]

Now let’s use partial application instead of defining shorten:

whatFollows c k str =
  map tail (filter match (map (take (k+1)) (tails str)))
  where match sub = take 1 sub == [c]

Let’s use . and $ to eliminate some of those parentheses:

whatFollows c k str =
  map tail . filter match . map (take (k+1)) $ tails str
  where match sub = take 1 sub == [c]

We can also replace match with a lambda:

whatFollows c k string =
  map tail . filter (\sub -> take 1 sub == [c])
           . map (take (k+1)) $ tails string

Finally, we don’t need to mention the string parameter at all, since we can just express whatFollows as a composition of map, filter, map and tails:

whatFollows c k =
  map tail . filter (\sub -> take 1 sub == [c])
           . map (take (k+1))
           . tails

We can even go a bit further by rewriting the lambda using an operator section

    \sub -> take 1 sub == [c]
=== \sub -> (==[c]) (take 1 sub)
=== \sub -> (==[c]) ((take 1) sub)
=== \sub -> ((==[c]) . (take 1)) sub
=== ((==[c]) . (take 1))
=== ((==[c]) . take 1)

Now what we have left is:

whatFollows c k = map tail . filter ((==[c]) . take 1)
                           . map (take (k+1))
                           . tails

This is a somewhat extreme version of the function, but when used in moderation the techniques shown here can make code easier to read.

More Functional List Wrangling Examples

Here are some more examples of functional programming with lists. Let’s start by introducing a couple of new list functions:

-- take elements from a list as long as they satisfy
-- a predicate
takeWhile :: (a -> Bool) -> [a] -> [a]

-- drop elements from a list as long as they satisfy
-- a predicate
dropWhile :: (a -> Bool) -> [a] -> [a]
takeWhile even [2,4,1,2,3]   ==> [2,4]
dropWhile even [2,4,1,2,3]   ==> [1,2,3]

There’s also the function elem, which can be used to check if a list contains an element:

elem 3 [1,2,3]   ==> True
elem 4 [1,2,3]   ==> False

Using these, we can implement a function findSubstring that finds the earliest and longest substring in a string that consist only of the given characters.

findSubstring :: String -> String -> String
findSubstring chars = takeWhile (\x -> elem x chars)
                      . dropWhile (\x -> not $ elem x chars)
findSubstring "a" "bbaabaaaab"              ==> "aa"
findSubstring "abcd" "xxxyyyzabaaxxabcd"    ==> "abaa"

The function zipWith lets you combine two lists element-by-element:

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (++) ["John","Mary"] ["Smith","Cooper"]
  ==> ["JohnSmith","MaryCooper"]
zipWith take [4,3] ["Hello","Warden"]
  ==> ["Hell","War"]

Sometimes with higher-order functions it’s useful to have a function that does nothing. The function id :: a -> a is the identity function and just returns its argument.

id 3 ==> 3
map id [1,2,3] ==> [1,2,3]

This seems a bit useless, but you can use it for example with filter or dropWhile:

filter id [True,False,True,True]
  ==>  [True,True,True]

dropWhile id [True,True,False,True,False]
  ==>  [False,True,False]

Another very simple but sometimes crucial function is the constant function, const :: a -> b -> a. It always returns its first argument:

const 3 True ==> 3
const 3 0    ==> 3

When partially applied it can be used when you need a function that always returns the same value:

map (const 5) [1,2,3,4]
  ==> [5,5,5,5]

filter (const True) [1,2,3,4]
  ==> [1,2,3,4]

Lists and Recursion

Here’s a new operator, :

Prelude> 1:[]
[1]
Prelude> 1:[2,3]
[1,2,3]
Prelude> tail (1:[2,3])
[2,3]
Prelude> head (1:[2,3])
1
Prelude> :t (:)
(:) :: a -> [a] -> [a]

The : operator builds a list out of a head and a tail. In other words, x : xs is the same as [x] ++ xs. Why do we need an operator for this?

Actually, : is the constructor for lists: it returns a new linked list node. The other list constructor is [], the empty list. All lists are built using : and []. The familiar [x,y,z] syntax is actually just a nicer way to write x:y:z:[], or even more explicitly, x:(y:(z:[])). In fact (++) is defined in terms of : and recursion in the standard library.

Here’s a picture of how [1,2,3] is structured in memory:

Building a List

Using : we can define recursive functions that build lists. For example here’s a function that builds lists like [3,2,1]:

descend 0 = []
descend n = n : descend (n-1)
descend 4 ==> [4,3,2,1]

Here’s a function that builds a list by iterating a function n times:

iterate f 0 x = [x]
iterate f n x = x : iterate f (n-1) (f x)
iterate (*2) 4 3 ==> [3,6,12,24,48]

let xs = "terve"
in iterate tail (length xs) xs
  ==> ["terve","erve","rve","ve","e",""]

Here’s a more complicated example: splitting a string into pieces at a given character:

split :: Char -> String -> [String]
split c [] = []
split c xs = start : split c (drop 1 rest)
  where start = takeWhile (/=c) xs
        rest = dropWhile (/=c) xs
split 'x' "fooxxbarxquux"   ==>   ["foo","","bar","quu"]

Pattern Matching for Lists

Last lecture, it was said that constructors are things that can be pattern matched on. Above, it was divulged that the constructors for the list type are : and []. We can put one and one together and guess that we can pattern match on : and []. This is true! Here’s how you can define your own versions of head and tail using pattern matching:

myhead :: [Int] -> Int
myhead [] = -1
myhead (first:rest) = first

mytail :: [Int] -> [Int]
mytail [] = []
mytail (first:rest) = rest

You can nest patterns. That is, you can pattern match more than one element from the start of a list. In this example, we use the pattern (a:b:_) which is the same as (a:(b:_)):

sumFirstTwo :: [Integer] -> Integer
-- this equation gets used for lists of length at least two
sumFirstTwo (a:b:_) = a+b
-- this equation gets used for all other lists
-- (i.e. lists of length 0 or 1)
sumFirstTwo _       = 0
sumFirstTwo [1]      ==> 0
sumFirstTwo [1,2]    ==> 3
sumFirstTwo [1,2,4]  ==> 3

Here’s an example that uses many different list patterns:

describeList :: [Int] -> String
describeList []         = "an empty list"
describeList (x:[])     = "a list with one element"
describeList (x:y:[])   = "a list with two elements"
describeList (x:y:z:xs) =
  "a list with at least three elements"
describeList [1,3]
  ==> "a list with two elements"

describeList [1,2,3,4,5]
  ==> "a list with at least three elements"

List patterns that end with :[] can be typed out as list literals. That is, just like [1,2,3] is the same value as 1:2:3:[], the pattern [x,y] is the same as the pattern x:y:[]. Let’s rewrite that previous example.

describeList :: [Int] -> String
describeList []         = "an empty list"
describeList [x]        = "a list with exactly one element"
describeList [x,y]      = "a list with exactly two elements"
describeList (x:y:z:xs) =
  "a list with at least three elements"

Another way we can nest patterns is pattern matching on the head while pattern matching on a list. For example this function checks if a list starts with 0:

startsWithZero :: [Integer] -> Bool
startsWithZero (0:xs) = True
startsWithZero (x:xs) = False
startsWithZero []     = False

Consuming a List

Using pattern matching and recursion, we can recursively process a whole list. Here’s how you sum all the numbers in a list:

sumNumbers :: [Int] -> Int
sumNumbers [] = 0
sumNumbers (x:xs) = x + sumNumbers xs

Here’s how you compute the largest number in a list, this time using a helper function.

myMaximum :: [Int] -> Int
myMaximum [] = 0  -- actually this should be an error...
myMaximum (x:xs) = go x xs
  where go biggest [] = biggest
        go biggest (x:xs) = go (max biggest x) xs

Note:, go is just a cute name for the helper function here. It’s not special syntax.

It’s often convenient to use nested patterns while consuming a list. Here’s an example that counts how many Nothing values occur in a list of Maybes:

countNothings :: [Maybe a] -> Int
countNothings [] = 0
countNothings (Nothing : xs) = 1 + countNothings xs
countNothings (Just _  : xs) = countNothings xs
countNothings [Nothing,Just 1,Nothing]  ==>  2

Building and Consuming a List

Now that we can build and consume lists, let’s do both of them at the same time. This function doubles all elements in a list.

doubleList :: [Int] -> [Int]
doubleList [] = []
doubleList (x:xs) = 2*x : doubleList xs

It evaluates like this:

doubleList [1,2,3]
=== doubleList (1:(2:(3:[])))
==> 2*1 : doubleList (2:(3:[]))
==> 2*1 : (2*2 : doubleList (3:[]))
==> 2*1 : (2*2 : (2*3 : doubleList []))
==> 2*1 : (2*2 : (2*3 : []))
=== [2*1, 2*2, 2*3]
==> [2,4,6]

Once you know pattern matching for lists, it’s straightforward to define map and filter. Actually, let’s just look at the GHC standard library implementations. Here’s map:

map :: (a -> b) -> [a] -> [b]
map _ []     = []
map f (x:xs) = f x : map f xs

and here’s filter:

filter :: (a -> Bool) -> [a] -> [a]
filter _pred []    = []
filter pred (x:xs)
  | pred x         = x : filter pred xs
  | otherwise      = filter pred xs

Note! Naming the argument _pred is a way to tell the reader of the code that this argument is unused. It could have been just _ as well.

Tail Recursion and Lists

When a recursive function evaluates to a new call to that same function with different arguments, it is called tail-recursive. (The recursive call is said to be in tail position.) This is the type of recursion that corresponds to an imperative loop. We’ve already seen many examples of tail-recursive functions, but we haven’t really contrasted the two ways for writing the same function. This is sumNumbers from earlier in this lecture:

sumNumbers :: [Int] -> Int
sumNumbers [] = 0
sumNumbers (x:xs) = x + sumNumbers xs

In the second equation the function + is at the top level, i.e. in tail position. The recursive call to sumNumbers is an argument of +. This is sumNumbers written using a tail recursive helper function:

sumNumbers :: [Int] -> Int
sumNumbers xs = go 0 xs
  where go sum [] = sum
        go sum (x:xs) = go (sum+x) xs

Note the second equation of go: it has the recursive call to go at the top level, i.e. in tail position. The + is now in an argument to go.

For a function like sumNumbers that produces a single value (a number), it doesn’t really matter which form of recursion you choose. The non-tail-recursive function is easier to read, while the tail-recursive one can be easier to come up with. You can try writing a function both ways. The tail-recursive form might be more efficient, but that depends on many details.

However, when you’re returning a list there is a big difference between these two forms. Consider the function doubleList from earlier. Here it is again, implemented first directly, and then via a tail-recursive helper function.

doubleList :: [Int] -> [Int]
doubleList [] = []
doubleList (x:xs) = 2*x : doubleList xs
doubleList :: [Int] -> [Int]
doubleList xs = go [] xs
    where go result [] = result
          go result (x:xs) = go (result++[2*x]) xs

Here the direct version is much more efficient. The (:) operator works in constant time, whereas the (++) operator needs to walk the whole list, needing linear time. Thus the direct version uses linear time (O(n)) with respect to the length of the list, while the tail-recursive version is quadratic (O(n²))!

One might be tempted to fix this by using (:) in the tail-recursive version, but then the list would get generated in the reverse order. This could be fixed with an application of reverse, but that would make the resulting function quite complicated.

There is another reason to prefer the direct version: laziness. For now it’s enough for you to know that the direct way of generating a list is simpler, more efficient and more idiomatic. You should try to practice it in the exercises. Check out the standard library implementations of map and filter above, even they produce the list directly without tail recursion!

Something Fun: List Comprehensions

Haskell has list comprehensions, a nice syntax for defining lists that combines the power of map and filter. You might be familiar with Python’s list comprehensions already. Haskell’s work pretty much the same way, but their syntax is a bit different.

Mapping:

[2*i | i<-[1,2,3]]
  ==> [2,4,6]

Filtering:

[i | i <- [1..7], even i]
  ==> [2,4,6]

In general, these two forms are equivalent:

[f x | x <- lis, p x]
map f (filter p lis)

List comprehensions can do even more. You can iterate over multiple lists:

[ x ++ " " ++ y | x <- ["A", "B"], y <- ["C","D"] ]
  ==> ["AC","AD","BC","BD"]

You can make local definitions:

[ reversed | word <- ["this","is","a","string"],
             let reversed = reverse word ]
  ==> ["siht","si","a","gnirts"]

You can even do pattern matching in list comprehensions!

firstLetters string = [ char | (char:_) <- words string ]
firstLetters "Hello World!"
  ==> "HW"

Something Fun: Custom Operators

In Haskell an operator is anything built from the characters !#$%&*+./<=>?@\^|-~. Operators can be defined just like functions (note the slightly different type annotation):

(<+>) :: [Int] -> [Int] -> [Int]
xs <+> ys = zipWith (+) xs ys
(+++) :: String -> String -> String
a +++ b = a ++ " " ++ b

Self Checks

Check 1

What’s the type of this function?

both p q x = p x && q x
  1. a -> Bool -> a -> Bool -> a -> Bool
  2. (a -> Bool) -> (a -> Bool) -> a -> Bool
  3. (a -> Bool) -> (b -> Bool) -> c -> Bool

Check 2

What’s the type of this function?

applyInOut f g x = f (g (f x))`
  1. (a -> b) -> (b -> a) -> a -> b
  2. (a -> b) -> (b -> c) -> a -> c
  3. (a -> a) -> (a -> a) -> a -> a

Check 3

Which one of the following functions adds its first argument to the second?

  1. f x x = x + x
  2. f x = \y -> x + y
  3. f = \x y -> x + x

Check 4

Which one of these functions does not satisfy f 1 ==> 1?

  1. f x = (\y -> y) x
  2. f x = \y -> y
  3. f x = (\y -> x) x

Check 5

Which one of the following functions is correctly typed?

  1. f x y = not x; f :: (Bool -> Bool) -> Bool
  2. f x = x ++ "a"; f :: Char -> String
  3. f x = 'a' : x; f :: String -> String

Check 6

How many arguments does drop 2 take?

  1. Zero
  2. One
  3. Two

Check 7

What does this function do? f (_:x:_) = x

  1. Returns the first element of a list
  2. Returns an arbitrary element of a list
  3. Returns all except the first and last elements of a list
  4. Returns the second element of a list

Check 8

What is the result of the following expression?

reverse $ take 5 . tail $ "This is a test"`
  1. "i sih"
  2. "set a"
  3. A type error

Check 9

If f :: a -> b, then what is the type of map (.f)?

  1. [b -> c] -> [a -> c]
  2. [c -> a] -> [c -> b]
  3. (b -> c) -> [a -> c]
  4. [a] -> [b]

Check 10

What is the type of the leftmost id in id id?

  1. unspecified
  2. a
  3. a -> a
  4. (a -> a) -> (a -> a)

Check 11

What is the type of const const?

  1. unspecified
  2. (c -> a -> b) -> a
  3. c -> (a -> b -> a)
  4. a -> b -> c -> a

Acknowledgment:

This reading was originally written by Joel Kaasinen and John Lång in their open source textbook. The book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License, which allows users to copy, modify, and distribute the book.

This reading was modified by Titus H. Klinge in 2021 and presented above under the same license in order to better serve the students of this course.