Skip main navigation

£199.99 £139.99 for one year of Unlimited learning. Offer ends on 28 February 2023 at 23:59 (UTC). T&Cs apply

Find out more

Reduction, Functions and Lists

This article explains Haskell's computational model reduction and introduces the key concepts of functions and lists.
Roof of Kyoto railway station
© University of Glasgow


A model of program execution

  • A programmer needs a concrete model for how a program is executed.
  • For imperative programs, we can execute statement by statement, keeping track of the values of variables (the stack) and where we are in the program (the program counter).
  • Functional programs don’t have statements!
  • The mechanism for executing functional programs is reduction.


Reduction is the process of converting an expression to a simpler form. Conceptually, an expression is reduced by simplifying one reducible expression (called “redex”) at a time. Each step is called a reduction, and we’ll use -- > to show the result.


 3 + (4*5)
-- >
 3 + 20
-- >


Reduction is important because it is the sole means of execution of a functional program. There are no statements, as in imperative languages; all computation is achieved purely by reducing expressions.


Unique reduction path


When a reduction is performed, there is only one possible answer. In this example, the computation has only one possible path:


 3 + (5 * (8-2))
-- >
 3 + (5 * 6)
-- >
 3 + 30
-- >


There is only one possible reduction path in that example, because in each step the current expression contains only one redex.


Multiple reduction paths


If an expression contains several redexes, there will be several reduction paths.


(3+4) * (15-9)
-- >
 7 * (15-9)
-- >
 7 * 6
-- >


(3+4) * (15-9)
-- >
 (3+4) * 6
-- >
 7 * 6
-- >


The result doesn’t depend on reduction path!


A fundamental theorem (the Church-Rosser theorem):


Every terminating reduction path gives the same result


This means that



    • Correctness doesn’t depend on order of evaluation.


    • The compiler (or programmer) can change the order freely to improve performance, without affecting the result.


    • Different expressions can be evaluated in parallel, without affecting the result. As a result, functional languages are leading contenders for programming future parallel systems.





Haskell is a functional language so the function concept is essential to the language. A function takes one or more arguments and computes a result. Given the same arguments, the result will always be the same. This is similar to a mathematical function and it means that in Haskell there are no side-effects.
There are two fundamental operations on functions: function definition (creating a function) and function application (using a function to compute a result).


Function definitions



    • In Haskell, many functions are pre-defined in a standard library called the prelude.


    • In due course, we’ll learn how to use many of these standard functions.



Defining a function



    • But the essence of functional programming is defining your own functions to solve your problems!


    • A function is defined by an equation.



f = x -> x+1 -- lambda function
-- or
f x = x+1 -- named function


This is equivalent to (f(x) = x+1) in mathematical notation.



    • The left hand side of the equation looks like a variable – and that’s what it is


    • The right hand side is an expression that uses the local variables listed in parentheses and defines the result of the expression.



Function application


How function application works



    • A function definition is an equation, e.g. (tt{f , =, backslash x rightarrow x+1})



    • The left hand side gives the name of the function;


    • The right hand side (the “body”) is an expression giving the formal parameters, and the value of the application. The expression may use the parameters.



    • An application is an expression like f 31, where (31) is the argument.


    • The application is evaluated by replacing it with the body of the function, where the formal parameters are replaced by the arguments.



Example of application


f = x - > x+1
 f 3
-- > {bind x=3}
 (x+1) where x=3
-- > {substitute 3 for x}
-- >


Multiple arguments and results


Functions with several arguments


A function with three arguments:


add3nums = x y z -> x + y + z


To use it,


10 + 4* add3nums 1 2 3
= {- put extra parentheses in to show structure -}
 10 + ( 4* (add3nums 1 2 3) )
 -- >
 10 + (4*(1+2+3) )
 -- >
 10 + (4*6)
 -- >
 10 + 24
 -- >




A key datastructure: the list



    • A list is a single value that contains several other values.


    • Syntax: the elements are written in square parentheses, separated by commas.



['3', 'a']
[2.718, 50.0, -1.0]


Function returning several results



    • Actually, a function can return only one result.


    • However, lists allow you to package up several values into one object, which can be returned by a function.


    • Here is a function (minmax) that returns both the smaller and the larger of two numbers:



minmax = x y -> [min x y, max x y]
minmax 3 8 -- > [3,8]
minmax 8 3 -- > [3,8]


The elements are evaluated lazily


You can write a constant list


mylist = [2,4,6,8]


But the elements can be expressions. They are evaluated only when they are used. Suppose you define:


answer = 42
yourlist = [7, answer+1, 7*8]




yourlist -- > [7, 43, 56]


But as long as you do not access the expression, it is not evaluated.


Constructing lists


Append: the ((++)) operator



    • The ((++)) operator takes two existing lists, and gives you a new one containing all the elements.


    • The operator is pronounced append, and written as two consecutive + characters.



[23, 29] ++ [48, 41, 44] -- > [23, 29, 48, 41, 44]


A couple of useful properties:



    • The length of the result is always the sum of the lengths of the original lists.


    • If (xs) is a list, then ([] ++ xs = xs = xs ++ []).






    • Sometimes it’s useful to have a sequence of numbers.


    • In standard mathematical notation, you can write (0, 1, ldots, n).


    • Haskell has a sequence notation for lists.


    • Write the sequence in square brackets, with start value, the operator .., and end value.


    • [0 .. 5] -- > [0,1,2,3,4,5]


    • [100 .. 103] -- > [100,101,102,103]


    • The elements are incremented by 1



Sequences aren’t limited to numbers



    • There are many enumerable types where there is a natural way to increment a value.


    • You can use sequences on any such type.


    • Characters are enumerable: there is a successor to each character.



For example:


[a .. z] 
-- > 
[a,b,c,d,e,f,g,h,i,j,k, l,m,n,o,p,q,r,s,t,u,v, w,x,y,z]


is a list of characters;


[0 .. 9]
-- > 


is a list of characters (which happen to be the digit characters);


[0 .. 9] 
-- > 


is a list of numbers.


List comprehensions



    • A list comprehension is a high level notation for specifying the computation of a list


    • The compiler automatically transforms a list comprehensions into an expression using a family of basic functions that operate on lists


    • List comprehensions were inspired by the mathematical notation set comprehension.



    • Examples of set comprehensions:
        • A set obtained by multiplying the elements of another set by 3 is
          ({3 times x ;|;x leftarrow {1, ldots, 10}}).


        • The set of even numbers is
          ({2 times x ;|; x leftarrow N }).


        • The set of odd numbers is

          ({2 times x + 1 ;|; x leftarrow N }).


        • The cross product of two sets A and B is
          ({(a,b) ;|;a leftarrow A, b leftarrow B}).





Examples of list comprehensions


[3*x | x <- [1..10]]
-- >

[2*x | x <- [0..10]]
-- >

[2*x + 1 | x <- [0..10]]
-- > 

[[a,b] | a <- [10,11,12] , b <- [20,21]]
-- >


Operating on lists


Indexing a list



    • We can index a list by numbering the elements, starting with 0.


    • Thus a canonical form of a list with (n) elements is ([x_0, x_1, .. x_{n-1}]).


    • The (!!) operator takes a list and an index, and returns the corresponding element.



[5,3,8,7] !! 2 -- > 8
[0 .. 100] !! 81 -- > 81
['a'..'z'] !! 13 -- > 'n'



    • If the index is negative, or too large, undefined is returned.


    • For robust programming, we need to ensure either that all expressions are well defined, or else that all exceptions are caught and handled.


    • Later, we’ll look at how to follow both of those approaches.



head and tail



    • There are standard library functions to give the head of a list (its first element) or the tail (all the rest of the list)


    • The result of applying head or tail to the empty list is undefined.



head :: [a] -> a
head [4,5,6] -- > 4
tail :: [a] -> [a]
tail [4,5,6] -- > [5,6]



    • Recommendation: avoid using (head) and (tail), because you want to avoid undefined values so your programs are robust. Unless you’re doing something really sophisticated, you’re better off with pattern matching. There are, however, some cases where they are appropriate.



Lists are lazy


We have mentioned before that Haskell is “lazy”, meaning that it only evaluates expressions when they are required for the evaluation of another expression.This behaviour extends to lists, so we can actually define infinite lists using sequences, for example [1 .. ] is the list of all positive integers. Another example is the primes function (from the Data.Numbers.Primes package) which returns an infinite list of prime numbers.
A consequence of laziness in lists is that you can define lists containing very complex and time consuming expressions, and as long as you never access them they will not be evaluated. The same is true for an incorrect expression, for example defining xs = [1,2,xs !! 5,4] will not result in an error as long as you don’t access the third element.


Keep in mind that lists are also immutable. As a result, if you define xs2 = xs ++ xs and try to access the third element xs2 !! 2 will still result in an error because xs has not been modified:


xs2 !! 2 -- > *** Exception: Prelude.(!!): index too large


Interestingly, if we change the definition of xs to xs = [1,2,xs2 !! 5,4], then both xs !! 2 and xs2 !! 2 will return 2:


xs = [1,2,xs2 !! 5,4]
xs2 = xs ++ xs
xs2 !! 2 -- > 2
xs !! 2 -- > 2

This is a consequence of Haskell’s expression evaluation through reduction: the order of the expressions does not matter.

© University of Glasgow
This article is from the free online

Functional Programming in Haskell: Supercharge Your Coding

Created by
FutureLearn - Learning For Life

Our purpose is to transform access to education.

We offer a diverse selection of courses from leading universities and cultural institutions from around the world. These are delivered one step at a time, and are accessible on mobile, tablet and desktop, so you can fit learning around your life.

We believe learning should be an enjoyable, social experience, so our courses offer the opportunity to discuss what you’re learning with others as you go, helping you make fresh discoveries and form new ideas.
You can unlock new opportunities with unlimited access to hundreds of online short courses for a year by subscribing to our Unlimited package. Build your knowledge with top universities and organisations.

Learn more about how FutureLearn is transforming access to education