Skip to content
UoL CS Notes

COMP105 (Haskell Functional Programming)

Lecture 29

COMP105 Lectures

This lecture covers the answers to last year’s class test which is available here.

2019 Class Test

Question 6

Tail recursion is when once you have recursed there is nothing left to process. Having an accumulator is a sign that this is the case.

Lecture 27

COMP105 Lectures

This lecture is on tail recursion.

Strict Evaluation of factorial

fact 1 = 1
fact n = n * fact (n-1)

When calling fact 4 under strict evaluation the computer will compute the following:

4 * fact 3
4 * (3 * fact 2)
4 * (3 * (2 * fact 1))
4 * (3 * ( 2 * 1))
24

This will make a very large tree of values to be evaluated

Recursion and Memory

Recursion seems to use a lot of memory.

4 * (3 * (2 * fact 1))

This is the call stack:

  • Each recursive call adds a new value to the stack.
  • We can only evaluate the stack then we hit a base case.

We will run out of memory if the call stack it too big.

Tail Recursion

This is the solution to the problem with running our of memory.

A function is tail recursive if there is nothing left to do after the recursive call.

If there is something to do after the recursive call then it is not tail recursive.

is_even 0 = True
is_even 1 = False
is_even n = is_even (n-2)

This is tail recursive as there is nothing added to the call stack. We are passing a single value and not generating additional values.

  • There is nothing left to do after the recursive call.
  • No call stack is built up.
  • Less memory is used.

This is an important concept in all languages:

  • Even for most imperative languages.
  • Most C compilers will optimize tail recursive calls.

Writing Tail Recursive Functions

We can make functions tail recursive:

  • By adding an accumulator as an argument.
  • It is initialized to some value.
  • Each recursive call modifies it.
  • Just like for folds and scans.
fact_tail 1 acc = acc
fact_tail n acc = fact_tail (n-1) (acc * n)

factorial n = fact_tail n 1

This is the same function as before but implemented with a helper function that is tail recursive. You can see that it uses a running total so that it doesn’t have to store many values and then add them together later.

No call stack built up, so no problems in strict evaluation.

In lazy evaluation the memory problem is just transferred to the accumulator.

fact_tail in Lazy Evaluation

If we call factorial 4 in a lazily evaluated language:

fact_tail 4 1
fact_tail 3 (4*1)
fact_tail 2 (3*4*1)
fact_tail 1 (2*3*4*1)
24

You can see that the memory problem is still here in lazy evaluation.

Fixing fact_tail

We can fix this with the $! (strict evaluation) operator.

fact_tail 1 acc = acc
fact_tail n acc = fact_tail (n-1) $! (acc * n)

factorial n = fact_tail n 1

Now there are no memory problems as the accumulator is evaluated strictly.

This is only okay because we know that we are going to evaluate the accumulator for sure.

Left Folds via Tail Recursion

foldl is naturally implemented via tail recursion.

  • In strict languages, foldl should be preferred over foldr.
foldl f acc []     = acc
foldl f acc (x:xs) = foldl f (f acc x) xs
> foldl (=) 0 [1,2,3,4,5]
15

Strict Left Folds

The previous implementation is strict but will have memory issues in Haskell. foldl' is the strict version of foldl.

  • It is in the Data.List package.

It is implemented like so:

foldl' f acc [] = acc
foldl' f acc (x:xs) = (foldl' f $! (f acc x)) xs

This will usually use less memory than foldl.

This is saying that f is lazy f acc x is strict and xs is lazy.

Left Folds and Laziness

A left fold can destroy laziness.

  • We must reach the end of the list to get the accumulator.

    This mean that if you’re operating on an infinite list then your program will never terminate.

Here is a binding that generates an infinite list.

> let f = foldr (\ x acc -> x : acc) [] [1..]
> take 4 f
[1,2,3,4
]

If you rewrite this using a left fold then it won’t terminate.

> let g = foldl(\ acc x -> acc ++ [x]) [] [1..]
> take 4 g
<will never terminate>

You shouldn’t use foldl if you are going to evaluate an infinite list.

Summary

foldr

  • Should be used by default in Haskell.
    • Because it preserves laziness.

foldl

  • There is not real reason to use this in Haskell.

foldl'

  • Should be used when we know that laziness won’t help.
    • If we know that we will fold the entire list.
  • Can have performance benefits.

Exercises

  1.  sum_helper acc [] = acc
     sum_helper acc (x:xs) = (sum_helper $! (acc + x)) xs
     sum list = sum_helper 0 list
    
  2.  length_helper acc [] = acc
     length_helper acc (_:xs) = (length_helper $! (acc + 1)) xs
     length list = legnth_helper 0 list
    
  3.  length = foldl' (\ acc _ -> acc + 1) 0
    

Lecture 26-2

COMP105 Lectures

Lazy Lists

Lists are never evaluated until they are needed. This means that take 1 [1..10] is just as efficient as take 1 [1..1000].

Infinite Lists

The reason why we are able to make infinite lists is a result of lazy evaluation. As evaluation only happens right at the end, the program won’t hang by trying to evaluate it in the background. You can make simple infinite list like so:

[1,1..]

Or like this:

all1s :: [Int]
all1s = 1 : all1s

As a result of this you are able to do infinite computations on infinite lists provided that you can resolve to a finite value.

all2s = zipwith (+) all1s all1s

This will produce an infinite sum of infinite lists.

Example

sieve (x:xs) = x : filter (\y -> y `mod` x /= 0) (sieve xs)

You can then use this function to define other useful infinite lists:

primes = sieve [2..]
> take 10 primes
[2,3,]
> primes !! 1000
7927

The $ Operator

The $ function evaluates a function.

Take this function and apply it to this argument

Strict Evaluation

The $! operator makes Haskell evaluate strictly:

> let f x = 1

> f $ undefined
1

> f $! undefined
Error

For most code you won’t notice the difference but it can change the error outputs.

Lecture 26-1

COMP105 Lectures

Evaluation Strategies

inc x = x + 1
square x = x * x

So square (inc 2) = (2 + 1) * (2 + 1) = 9 . How does Haskell evaluate this?

Strict Evaluation

In strict evaluation, we always apply the innermost function first:

square (inc 2)
	square (3)
	3 * 3
	9

Here we first apply inc and then apply square.

This means that when we call a function we first evaluate its argument.

This is the method that imperative languages use to evaluate functions. Haskell does not evaluate this way

Lazy Evaluation

In lazy evaluation, we always apply the outermost functions first:

square (inc 2)
	inc 2 * inc 2
	3 * 3
	9

First apply square then apply inc.

This method waits as long as possible until computing values.

In the following, nothing is computed until we ask for the value of z.

> let x = 1 + 1
> let y = x + x
> let z = y / 2
> z
2.0
> let x = 1 + undefined
> let y = x + x
> let z = y / 2
> z
*** Exception: Prelude.undefined

You can see here that the error only occurs when the binding is evaluated.

If a value is never used, it is never computed:

> let f x = 1
> f undefined
1

You can see here that the argument is not evaluated. This results in the program not crashing due to evaluating undefined.

Imperative Languages

Imperative languages are always strict:

  • So programmers know the order of their side effects.

Functional Languages

For pure functions the order of evaluation is irrelevant.

  • You always get the same answer.

  • No need to worry about side effects.

Some functional languages are strict by default:

  • ML
  • Ocaml

Others are lazy by default:

  • Haskell

Lazy vs. Strict

If strict evaluation finds an answer, the lazy evaluation fill find the same answer.

Sometimes lazy evaluation can find an answer when strict evaluation cannot:

> fst (1, 1 `div` 0)
1

A strict evaluator would crash in this example.

Sometimes lazy evaluation can be more efficient than strict:

  • Particularly if some values are computed but never used.

Lazy evaluation only ever computed a value when it is needed. This can lead to big efficiency savings:

  • When computing the the head of a doubled list.

Summary

Strict evaluation:

  • Evaluate things as soon as possible.
  • Gives certainty over order of side effects.

Lazy evaluation:

  • Evaluate things only when they are needed.
  • Potentially eliminates unneeded computation.

Exercise

  1. 1
  2. [5,10,*** Exception: divide by zero

    This is due to the fact that it evaluates last so will print the first two elements. This is opposed to crashing immediately.

  3. *** Exception: divide by zero

Lecture 25

COMP105 Lectures

IO Example

In this lecture we are covering an extended example on making ASCII art of a given input char. It will add chars onto the resultant output when you give them as input.

As this is an example lecture I will be taking notes on key concepts, for the full example see the slides.

The source code for this example is given here.

Printing Text-Mode Graphics

This is a stub, see the slides above for more details.

Updating the Screen

Interactive Loop

This is a stub, see the slides above for more details.

Main Function

This is a stub, see the slides above for more details.

Lecture 24-2

COMP105 Lectures

Useful IO Functions

This lecture covers many useful functions included with Haskell. I won’t be writing out examples but I will be writing down names and descriptions to search through.

For more detailed examples view the slides for this lecture.

print

This prints out any show-able type.

putStr

This prints a string without adding a new line.

With this you have to manually end the line with:

putStr '\n'

readLn

This function gets a line of input and then calls read. This saves you calling read when getting a line with getLine.

To give an explicit type use the syntax:

x <- readLn :: IO Int

This is due to the fact that when readLn is reading the line it is still in the IO ‘box’.

forever

This repeat an IO action forever. It is in the Control.Monad package. This is a higher order IO action as it takes another IO action as an input.

It can be used to make interactive code that runs a function forever.

sequence

This IO action will run a list of IO actions and will return a list containing the return values of the IO actions.

The IO actions must have the same type otherwise they can’t be put in the same list.

sequence works well with map:

sequence (map print [1..10])

This saves you typing out all the IO actions to sequence on.

sequence_

This IO action is the same as sequence but it will return the type IO (). You would want to use this when the IO actions you are running return IO () themselves.

mapM

You can use this map on monads. This is similar to running sequence in conjunction with map. This is the same as the code from before:

mapM print [1..10]

mapM_

This is similar to sequence_.

when

This executes an IO action if a condition is true.

when True (putStrLn "hi")

This is similar to if in pure functional code.

unless

This is the opposite of when. It will execute if the condition is false.

Exercises

  1.  timesTwo = do
         n <- readLn :: IO Int
         print (n * 2)
    
  2.  import Control.Monad
     ltFiveOnce = do
         x <- getLine
         when (length x < 5) (putStrLn "yes")
         when (length x >= 5) (putStrLn "no")
     ltFive = forever ltFiveOnce
    
  3.  addThree = do
     x <- sequence [readLn,readLn,readLn]
     print (sum x)
    

Lecture 24-1

COMP105 Lectures

Writing Programs

To write a program in Haskell we write a main function.

main :: IO ()
main = putStrLn "Hello world!"

main always:

  • Takes no arguments
  • Returns an IO type.

To run the program, we first need to compile it:

 $ ghc -dynamic hello.hs 
[1 of 1] Compiling Main ( hello.hs, hello.o )
Linking hello ...
 $ ./hello

As I am running arch Linux with dynamically linked libraries I must use the -dynamic flag when compiling. For distros with static libraries this is not required.

Command Line Arguments

Most command line programs take arguments:

  • getArgs :: IO [String] returns those arguments.
  • This function is in System.Environment
import System.Environment

main :: IO ()
main = do
    args <- getArgs
    let output = "Command line arguments: " ++ show args
    putStrLn output

This function will return the command line arguments as a list of strings.

Looping in IO Code

The only way to loop in IO code is to use recursion.

printN :: String -> Int -> IO ()

printN _ 0 = return ()
printN str n =
    do
        putStrLn str
        printN str (n-1)

This program will write back a given string n times to separate lines.

  • No variables.
  • No loops.

Using Command Line Arguments

import System.Environment
main :: IO ()
main = do
    args <- getArgs
    let n = read (args !! 0) :: Int
    printN (args !! 1) n
 $ ./repeat_string 3 hello
hello
hello
hello

File IO

readFile will read the contents of a file:

readFile :: String -> IO String

Suppost that example.txt contains:

line one
line two
line three
> readFile "example.txt"
"line one\nline two\nline three\n"

writeFile writes a string to a file.

writeFile :: String -> String IO ()

The file will be overwritten!

> writeFile "output.txt" "hello\nthere\n"

The file output.txt will then contain:

hello
there

Finishing the marks.csv Example

We wrote the report function in Lecture 18-1. Now we can turn it into a program:

import System.Environment
main :: IO ()
main = do
    args <- getArgs
    let infile = args !! 0
        outfile = args !! 1
    input <- readFile infile
    writeFile outfile (report input)

Exercises

  1.  import System.Environment
     main :: IO ()
     main = do
         args <- getArgs
         infile <- readFile (args !! 0)
         let string = (lines infile) !! 0
         putStrLn string
    
  2.  import System.Environment
     main :: IO ()
     main = do
         args <- getArgs
         let num = read (args !! 0) :: Int
         putStrLn $ show (num + 1)
    
  3.  main :: IO ()
     main = do
         putStrLn "Write a line of text to be written to output.txt."
         string <- getLine
         writeFile "output.txt" (string ++ ['\n'])
    

    Remember to end files with \n so that they are read properly.

Lecture 23-2

COMP105 Lectures

Writing IO Code

We can write our own IO actions:

print_two :: String -> String -> IO ()
Print_two s1 s2 = putStrLn (s1 ++ s2)
> print_two "abc" "def"
abcdef

Note that the return type is IO ().

Combining Multiple IO Calls

The do syntax allows us to combine multiple IO actions.

get_and_print :: IO ()
get_and_print =
    do
        x <- getLine
        y <- getLine
        putStrLn (x ++ " " ++ y)

This syntax allows us two write a sequence of statements that will be executed in order.

You could also write this with the following syntax:

get_and_print :: IO ()
get_and_print = do
    x <- getLine
    y <- getLine
    putStrLn (x ++ " " ++ y)

The do Syntax

A do block has the following syntax:

do
    v1 <- [IO action]
    v2 <- [IO action]
    ...
    vk <- [IO action]
    [IO action]
  • v1 through vk unbox the results of IO actions.
  • The final IO action is the return value.

The v <- portion can be skipped if you are using an IO action purely for the side effect.

let in do Blocks

let expressions can be used inside do block in order to complete pure computations:

add_one :: IO ()
add_one =
    do
        n <- getLine
        let num = (read n) :: Int
            out = show (num + 1)
        putStrLn out

In a do block you can type anything out that you would type into the command line of GHCI.

if in do Blocks

guess :: IO ()
guess = do
    x <- getLine
    if x == "42"
        then putStrLn "Correct!"
        else putStrLn "You are very wrong."

Both branches of the if mut have the same type.

do Blocks

do blocks let you sequence multiple actions:

  • Works with IO actions.
  • Will not work in pure functional code.

Functional programs consist of:

  • A small amount of IO code.
  • A large amount of pure functional node.

Don’t try to write your entire program in IO code.

Putting Values in the IO Box

Sometimes we need to pus a pure value into IO.

  • We can use the return function to do this.
> :t "hello"
"hello" :: [Char]

> :t return "hello"
IO [Char]
example :: IO String
example = do
    x <- getLine
    return (tail x)

You must use return to get the value out of an impure function.

print_if_short :: String -> IO ()
print_if_short str =
    if length str <= 2
        then putStrLn str
        else return ()

Both sides of the if must have the type IO ():

  • So we use return () in the else part.

return

This function is not the same as in imperative languages return does not stop execution. It just convert pure values to IO values.

Monad

The type of return mentions monads.

> :t return
return :: Monad m => a -> m a

This is because IO is a monad.

  • Whenever you see Monad m => substitute IO for m.
  • So return :: a -> IO a

You don’t need to know anything about monads for COMP105.

Exercises

  1.  doubleEcho :: IO ()
     doubleEcho = do
         x <- getLine
         putStrLn x
         putStrLn x
    
  2.  firstWord :: IO ()
     firstWord = do
         string <- getLine
         if (words string) == []
             then putStrLn ""
             else putStrLn (head . words $ string)
    
  3.  printEven :: Int -> IO ()
     printEven x = if (even x) 
         then (putStrLn x)
         else return ()
    

    This doesn’t need to be done in a do block as it is just one line.

Lecture 23-1

COMP105 Lectures

So far, we have studied pure functional programming. Pure functions:

  • Have no side effects.
  • Always return a value.
  • Are deterministic.

All computation can be done in pure functional programming.

IO

Sometimes programs need to do non-pure things:

  • Print something to the screen.
  • Read or write a file.

IO v.s. Pure Functional

graph LR
IO --> p[Pure Functional]
p --> IO
  • Impure IO code talks to the outside world.
  • Pure functional code does the interesting computation.
  • IO code can call pure functions; pure functions cannot call IO.

We can call the “functions” that complete IO: IO actions. This is because they are not functions.

getLine

getLine read a line of input from the console.

> getLine
hello there
"hello there"
> :t getLine
getLine :: IO String

The IO Type

This type marks a value as being impure.

If a function returns an IO type then it is impure:

  • It may have side effects.
  • It may return different values for the same inputs.

The IO type should be thought of as a box:

  • The box hold a value from an impure computation.
  • we can use <- to get an impure value from the IO type.
> x <- getLine
Hello
> x
"Hello"

Values must be unboxed before you use them in a pure function.

getChar

This function is similar to getLine but returns a single Char instead of a string.

putStrLn

This IO action prints a string onto the console.

> putStrLn "Hello"
Hello

There are no quotation marks as we are seeing this being written to the StdOut and not via read.

> :t putStrLn
putStrLn :: String -> IO ()

The unit type has the IO type indicating that it has a side effect.

The Unit Type

The unit type is a type represented by (). It only has one value which is itself.

This is used to indicate that nothing interesting is returned. An example of this is with putStrLn where is doesn’t return a value but does have the side effect of printing to the StdOut.

Exercise

  1. It will ask for two lines. It will then print out the two lines with a space between them back to the StdOut.
  2. It will read in a line with the expectation that there will be a number on it. It will then convert the line into an Int and print out the number + 1.
  3. Error due to mismatched types. putStrLn expects a String but an IO type was given.

Lecture 22-2

COMP105 Lectures

Trees

graph TD
a[ ] --> b[ ]
a --> c[ ]
b --> d[ ]
b --> e[ ]
d --> f[ ]
d --> g[ ]
c --> h[ ]
c --> i[ ]

A tree is composed of:

  • Leaf nodes.
    • Leaves have no children.
  • Branch nodes.
    • Has a child which is a leaf.

A Tree Type in Haskell

Any binary tree can be represented in this data type.

data Tree = Leaf | Branch Tree Tree deriving(Show)
graph TD
a[ ] --> b[ ]
a --> c[ ]
c --> h[ ]
c --> i[ ]
Branch Leaf (Branch Leaf Leaf)

Recursion on Trees

We can write recursive function that process tress.

  • Usually the recursive case will process both branches.

This function counts all the nodes in a tree:

size :: Tree -> Int

size (Leaf) = 1
size (Branch x y) = 1 +size x + size y

For trees usually the base case is the Leaf and the recursive rule operates on the two sub-trees of the branch node.

> size (Branch Leaf (Branch Leaf Leaf))
5

Trees with Data

Nodes in a tree often hold data.

graph TD
9 --> 4
4 --> 1
4 --> 6
1 --> 10 
1 --> 7
2 --> 3 
2 --> 0
9 --> 2

Type of Trees with Data

This type allows each branch and leaf to have data of a single type associated with it:

data DTree a = DLeaf a
             | DBranch a (DTree a) (DTree a)
             deriving (Show)
graph TD
1 --> 7
7 --> 2 
7 --> 9
1 --> 4
DBranch 1 (DBranch 7 (DLeaf 2) (DLeaf 9)) (DLeaf 4)

Recursion on Trees with Data

This function adds together the numbers from all the branches and leaves in a tree with data.

tree_sum :: Num a => DTree a -> a

tree_sum (DLeaf x)       = x
tree_sum (DBranch x 1 l) = x + tree_sum l + tree_sum r
> tree_sum (DBranch 1 (DBranch 7 (DLeaf 2) (DLeaf 9)) (DLeaf 4))
22

Example - Fibonacci Numbers

Before we used this code to calculate the Fibonacci numbers. This is slow due to the large amount of branching:

fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
graph TD
A[fib 4] --> B[fib 3]
A --> C[fib 2]
B --> D[fib 2]
B --> E[fib 1]
D --> F[fib 1]
D --> G[fib 0]
C --> H[fib 1]
C --> I[fib 0]
How many recursive calls does the code make?

We can make a function to build the call tree.

fib_tree :: Int -> Tree

fib_tree 0 = Leaf
fib_tree 1 = Leaf
fib_tree n = Branch (fib_tree (n-1)) (fib_tree (n-2))
> fib_tree 4
Branch (Branch (Branch Leaf Leaf) Leaf) (Branch Leaf Leaf)

This is the same structure as the tree at the top of the example.

To count the nodes in this tree we can use the function size from earlier:

fib_calls n = size (fib_tree n)
> fib_calls 10
177
> fib_calls 20
21891
>fib_calls 30
2692537

As we can see multiple recursion can be very slow if used incorrectly.

Example - Finding a File

Suppose that we have a directory structure:

graph TD
h["~/"] --> d["docs/"]
d --> a[a.txt]
d --> b[b.txt]
h --> c[c.txt]

Write a function that, given a filename, finds the path to that file.

We can formulate the files as a data tree:

let fs =
    DBranch "~/"
        (DBranch "docs/" (DLeaf "a.txt" ) (DLeaf "b.txt"))
        (DLeaf "c.txt")

This is the same as the DTree above.

Note that the file might not exist:

  • So we will use the maybe type.
> find_file "a.txt" fs
Just "home/docs/a.txt"

> find_file "d.txt" fs
Nothing
Implementation
find_file file (Dleaf x)
    | x == file = Just file
    | otherwise = Nothing
    
find_file file (DBranch x l r) =
    let
        left  = find_file l
        right = find_file r
    in
        case (left, right) of
            (Just y, _) -> Just (x ++ y)
            (_, Just y) -> Just (x ++ y)
            (_, _)      -> Nothing

This implementation has two cases, one for the leaf and one for the branch.

Exercises

  1.  leafSum :: DTree Int -> Int
     leafSum (DLeaf x)       = x
     leafSum (DBranch _ l r) = leafSum l + leafSum r
    
  2.  treeElem :: Eq a => a -> DTree a -> Bool
     treeElem x (DLeaf y) = x == y
     treeElem x (DBranch y l r) = (x == y) || treeElem x l || treeElem x r
    

Lecture 22-1

COMP105 Lectures

Recursive Types

We have seen types which contain other types. In a recursive custom type, some constructors contain the type itself.

data IntList = Empty | Cons Int IntList deriving(Show)

Some examples using this type:

Empty -- []
Cons 1 (Empty) -- [1]
Cons 1 (Cons 2 Empty) -- [1,2]

Here is a more general list using a type variable:

data List a = Empty | Cons a (List a) deriving(show)

This is the same as the previous example but using any type (provided that it is the same type all the way through the list). To use this as an infix operator like : you would do the following:

> 1 `Cons` (2 `Cons` Empty)
Cons 1 (Cons 2 Empty)

Functions with Recursive Types

We can write functions for our custom list type:

our_head :: List a -> a

our_head Empty		= error "Empty list"
our_head (Cons x _)	= x

This takes the first item in our custom list type and prints it.

To get the tail you would use this function:

our_tail :: List a -> List a

our_tail Empty		= error "Empty list"
our_tail (Cons _ x)	= x

Recursing on Recursive Types

We can also write recursive functions to process our recursive types. This is exactly the same as recusing on a list:

our_sum :: List Int -> Int

our_sum Empty 		= 0
our_sum (Cons x xs) = x + our_sum xs

Custom Lists

Here is a new list type that can contain two different types.

data TwoList a b = TwoEmpty
				| ACons a (TwoList a b)
				| BCons b (TwoList a b)
					deriving(show)

This is the type when making a list that contains a Char and a Bool:

> :t 'a' `ACons`(False `BCons` TwoEmpty)
TwoList Char Bool

This is not the same as a list with tuples as the next item in the list can be of type a or type b not of both all the time. There is a cost also as you have to explicitly say if it is type a or type b.

Exercises

  1.  ourElem :: Eq a => List a -> a -> Bool
     ourElem Empty _ = False
     ourElem (List x xs) y
         | x == y 	= True
         | otherwise = ourElem xs
    
  2.  ourRange :: Int -> List Int
     ourRange 0 = Empty
     ourRange x = Cons x (ourRange (x - 1))
    
  3.  data ThreeList a b c = ThreeEmpty
                          | ACons a (ThreeList a b c)
                          | ACons b (ThreeList a b c)
                          | ACons c (ThreeList a b c)
                          deriving(show)
    

Lecture 21-2

COMP105 Lectures

Maybe

data Maybe a = Just a | Nothing
> :t Just "hello"
Maybe [Char]
> :t Just False
Maybe Bool
> :t Nothing
Maybe a

From here you can see that when you call the Just constructor you force the type variable a to be a particular type and if you call Nothing then no type is assigned to a.

The Maybe type is used in pure functional code that might fail.

safe_head [] = Nothing
safe_head (x:_) = Just x
> safe_head [1,2,3]
Just 1 
> safe_head []
Nothing

Given that the function will always give an output this means that it wont give an exception and crash the program.

Example

safe_get_heads list =
    let
        mapped = map safe_head list
        filtered = filter (/=Nothing) mapped
        unjust = (\ x -> case x of Just a -> a)
    in
        map unjust filtered
> safe_get_heads [[],[1],[2,3]]
[1,2]

This code will not crash in the event that an empty list is given to head.

Exceptions in Haskell

Haskell does include support for exceptions

> head []
*** Exception: Prelude.head: empty list

Exceptions are not pure functional:

  • Every function returns exactly one value.
  • You can’t catch exceptions in pure functional code.
  • Exceptions are mostly used in IO code.

The Maybe type provides a way to do exception-like behaviour in pure functional code.

Can this function fail for some inputs?

  • Use the Maybe type.

Exceptions should only be used in IO code:

  • File not found, could not connect to server, etc.
  • These are unpredictable events.

Either

data Either' a b = Left a | Right b
> :t Left 'a'
Either Char 'b'
> :t Right 'b'
Either a Char

The Either type is useful if you want to store different types in the same list:

is_left (Left _)    = True
is_left _           = False
> let list = [Left "one", Right 2, Left "three", Right 4]
> map is_left list
[True,False,True,False]

Example - Filtering off Left

get_lefts list = 
    let
        filtered = filter is_left list
        unleft = (\ (Left x) -> x)
    in
        map unleft filtered
> get_lefts list
["one","three"]

Example - Squaring Mixed Number Types

square (Left x) = Left (x ** 2)
square (Rightx) = Right (x ^ 2)
> let nums = [Left pi, Right (4::Int), Left 2.7182]
> map square nums
[Left 9.86,Right 16,Left 7.38]

Meaningful Error Messages

Either can also be used to give detailed error messages.

safe_head_either []     = Right "empty list"
safe_head_either (x:xs) = Left x
> safe_head_either []
Right "empty list"
> safe_head_either [1,2,3]
Left 1

This is required as a function can’t return values of two different types.

Exercises

  1.  safeTail :: [a] -> Maybe [a]
     safeTail []     = Nothing
     safeTail x:xs   = Just xs
    
  2.  safeDiv :: Int -> Int -> Maybe Int
     safeDiv _ 0 = Nothing
     safeDiv x y = Just (div x y)
    
  3.  safeGet :: [a] -> Int Either a String
     safeGet x y
         | x < 0 = Right "Negative index."
         | length x <= y = Right "Index too large."
         | otherwise = Left (x !! y)
    

Lecture 21-1

COMP105 Lectures

Parameterised Custom Types

Type Variables in Custom Types

We can use type variables in custom types:

data Point a = Point a a

Here we say we are making a new type where the constructor point is made of type a.

> :t Point (1::Int) (2::Int)
Point Int

We can also use multiple variable in the same type.

data Things a b c = Things a b c deriving (Show)
> Things "string" 1 True
Things "string" 1 True

When using a type that includes type variables the resultant type has its own type based on the types of data it was given. This links to how you can’t compare lists of two different types.

We can write functions using these types:

first_thing (Things x _ _) = x
> first_thing (Things 1 2 3)
1
> :t first_thing
first_thing :: Things a b c -> a

case Expressions

case expressions allow you to do pattern matching inside of functions.

data Example = One Int | Two Float

f :: Example -> Int
f x = case x of One a -> a
                Two b -> 0
> f (One 3)
3
> f (Two 4.0)
0

Example

Using case you can write functions that formerly would have been implemented in pattern matching:

f [] = 0 
f (x:xs) = 1 + f xs

f' list = case list of  []      -> 0
                        (x:xs)  -> 1 + f xs

Syntax

The syntax for a case expression is:

case [expression] of    [pattern 1] -> [expression]
                        [pattern 2] -> [expression]
                        ...
                        [pattern k] -> [expression]

All patterns should be on the same column in order for Haskell to process the expression correctly.

Additionally you can use the _ as a catch all at the end of the expression.

You can put them all on one line using the following syntax:

case x of {One a -> a; Two b -> 0}

Finally case is an expression so you can treat its result as a value:

> (case 1 of 1 -> 1) + (case 2 of 2 -> 1)
2

Exercises

  1.  data BigThings a b c d = BigThings a b c d derive Show
    
  2.  middleTwo :: BigThings a b c d -> (b, c)
     middleTwo (BigThings _ x y _) = (x, y)
    
  3.  data Example = One Int | Two Float
     isOne :: Example -> Bool
     isOne x = case x of One _ -> True
                         _     -> False
    

Lecture 19-2

COMP105 Lectures

More Complex Custom Types

More Complex Constructors

More complex constructors can contain other types.

data Point = Point Int Int deriving (Show, Read, Eq)

This is saying that the type Point has one constructor called Point and in that constructor there are two Int. The constructor name has no relation to the type name.

It is common that if your type has only one constructor to call constructor the same name as the type.

> Point 1 4
> Point 1 4

> read "Point 10 10" : Point
> Point 10 10

> Point 2 2 /= Point 3 1
> True

It is also common to use pattern matching to work with complex constructors:

shift_up (Point x y) = Point x (y +1)
> shift_up (Point 1 1)
> Point 1 2

> :t shift_up
> shift_up :: Point -> Point

Example

This example completes a computation using almost entirely our own custom types. We are also using pattern matching on our types:

move :: Point -> Direction -> Point

move (Point x y) North = Point x (y+1)
move (Point x y) South = Point x (y-1)
move (Point x y) East = Point (x+1) y
move (Point x y) West = Point (x-1) y
> move (Point 0 0) North
> Point 0 1

More Complex Constructors Continued

Types can have multiple constructors each of which can have their own types:

data Shape = circle Float | Rect Float Float deriving (Show)
> :t Circle 2.0
> Circle 2.0 :: Shape

> :t Rect 3.0 4.0
> Rect 3.0 4.0 :: Shape

Example

area :: Shape -> Float

area (Circle radius) = pi * radius ** 2
area (Rect x y) = x * y
> area (Circle 2.0)
> 12.56371

> area (Rect 3.0 4.0)
> 12.0

Records

You can use data types to build custom records:

data Person = Person String String Int String

get_first_name	(Person x _ _ _) = x
get_second_name	(Person _ x _ _) = x
get_age		(Person _ _ x _) = x
get_nationality	(Person _ _ _ x) = x
> get_age (Person "joe" "bloggs" 25 "UK")
> 25

Record Syntax

To make things easier, Haskell provides a record syntax:

data Person = Person {	firstName :: String,
			secondName :: String,
			age :: Int,
			Nationality :: String}
			deriving (Show)
> Person "joe" "bloggs" 25 "UK"
> Person {firstName = "joe", secondName = "bloggs", age = 25, nationality = "UK"}

This is similar to the previous example where we made our own record type.

Records can be created out of order, whereas normal data types cannot.

data Example = Example {a :: String,
						b :: Int}
						deriving (Show)
> Example "one" 2
> Example {a = "one", b = 2}

> Example {b = 3, a = "zero"}
> Example {a = "zero", b = 3}

If you create an out of order function then you should put them in {} with their labels so that Haskell can identify them.

Example

This example takes a co-ordinate Point to locate the shape in 2D space.

data AdvShape = AdvCircle Point Float | AdvRect Point Point deriving (Show)

area' (AdvCircle _ radius) = pi * radius ** 2
area' (AdvRect (Point x1 y1) (Point x2 y2)) =
	let
		w = abs (x1 - x2)
		h = abs (y1 - y2)
	in
		fromIntergral (w * h)

Exercises

  1.  data Point = Point Int Int deriving (Show, Read, Eq)
     distance :: Point -> Int
     distance (Point x y) = x + y
    
  2.  data HTTPResponse 	= Data Int String 
                 | Error String
                 deriving (Show, Read, Eq)
    
  3.  data Student = Student {name :: String
                 address :: String
                 marks :: [Int]
                 }
                 deriving (Show, Read, Eq)
    

Lecture 19-1

COMP105 Lectures

Custom Types

There are two ways to make types in Haskell.

The type Keyword

The type keyword gives a new name to an existing type.

  • All types must start with capital letters.
type String' = [Char]

exclaim :: String' -> String'
exclaim str = str + "!"
> exclaim "hello"
> "hello!!

Type is useful when you want to give a meaningful name to a complex type.

type VoteResults = [(Int,String)]

results :: VoteResults

The data Keyword

The data keyword is used to create an entirely new type.

data Bool' = True | False
  • | should be read as OR.
  • Each of the values is a constructor.
    • Each constructor should start with a capital letter.

To find out more about a type you can use the GHCI :info command. For more information about a type constructor then you can use the type command.

Example

data Direction = North | South | East | West

rotate North = East
rotate East = South
rotate South = West
rotate West = North
> :t rotate
> rotate :: Direction -> Direction

Type Classes

By default, a new data type is not part of any type class. This means that running rotate will give an error. As is GHCI doesn’t know how to show it.

We can use the deriving keyword to fix this:

data Direction = North | South | East | West deriving (Show)

This will automatically put the type into the class Show and will allow it to print to the prompt.

Hakell can automatically implement the following type classes:

  • Show
    • Will print out the type as it is in the code.
  • Read
    • Will parse the type as it is in the code.
  • Eq
    • The natural definition of equality.
  • Ord
    • Constructors that come first are smaller.

You can add these to the tuple that deriving takes as input. You should include all type classes that make sense for your type so that you can use the functions you want.

Exercises

  1. type Marks = (String, [Int])
  2. data Colour = Red | Blue | Green deriving (Show, Read)
  3.  toRGB :: Colour -> (Float, Float, Float)
     toRGB Red = (1,0,0)
     toRGB Green = (0,1,0)
     toRGB Blue = (0,0,1)
    

    You should note that as this type isn’t in the class eq then you can’t use guard and equality testing on it.

Lecture 18-2

COMP105 Lectures

First Past the Post Example

This example covers a first past the post election. This means whoever gets the most votes wins. We are aiming to make a function that performs this task:

> winner ["red", "blue", "red", "red", "green"]
> "red"

Getting the Candidates

First we need to figure out who the candidates are:

uniq [] = []
uniq (x:xs) = x : uniq (filter (/=x) xs )

This function will remove duplicates from a list of strings. For every new element found the filter will remove all further occurrences from the rest of the list before recursing on the list.

Counting the Votes

This function counts the number of votes for a particular candidate:

count x list = length (filter (==x) list)

Vote Totals

This function will count all the votes for each candidate and put the number of votes and then the candidate in a list of tuples.

total votes = 
	let
		candidates = uniq votes
		f = (\ c -> (count c votes, c))
	in
		map f candidates

Comparing Candidates

Tuples are compared lexicographically. This means that each element is compared in turn to find which satisfies the function.

> max (3, "red") (2, "blue")
> (3,"red")

This means that if two candidates have the same number then the string is compared.

maximum

The function maximum takes a list and returns the largest item in the list:

> maximum [(3, "red"), (2, "blue"), (4, "green")
> (4, "green")

Finding the Winner

snd returns the second value in a tuple:

winner votes = snd . maximum . totals $ votes

Applying this satisfies the requirement:

> winner ["red", "blue", "red", "red", "green"]
> "red"

Alternative Vote Example

In the alternative vote system, voters rank the candidates:

  • In each round, the candidate with the least number of first preference votes is eliminated.
  • The winner is the last candidate left once all other have been eliminated.
> let votes = [["red", "blue", "green"],
				["blue", "green"],
				["green", "red"],
				["blue", "red"],
				["red"]]
> av_winner votes
> "red"

You don’t need many preferences and each list is a single person’s preferences.

See the slides for full examples.

Ranking the Candidates

sort sorts all of the items in a list and orders them from smallest to biggest.

Getting the First Choice Votes

head doesn’t accept empty lists as input. This should be taken into account when removing items from lists unevenly.

Final Function

All of the components give this function:

av_winner votes =
	let
		ranked = rank_candidates votes
		first = head ranked
	in
		if length ranked == 1
		then first
		else av_winner (remove_cand first votes)

Lecture 18-1

COMP105 Lectures

Marks to Report Example

This lecture covers a mini assignment example about converting a csv file containing students marks into a report containing the students averages. These are presented in the following format:

aaa		70	65	67	60
bbb		55	60	55	65
ccc		40	40	40	40
ddd		80	60	75	60
ccc		0	0	0	100

And should be transformed to be:

aaa		65.5
bbb		58.75
ccc		40.0
ddd		68.75
ccc		25.0

See the slides for the full examples.

Reading files in Haskell

We can read a file using readFile:

  • This is an IO function.
  • We will study this in more detail later on.
> readfile "marks.csv"
> "aaa		70	65	67	60\nbbb		55	60	55...

The \n character is the newline character.

lines

The lines function takes a string containing multiple lines into a list of strings. The complement to this function is the function unlines. This will do the opposite.

Parsing the File

Using the functions words and lines we can put the file into a list of lists of strings, in order to process the file.

Getting the Averages

The function read will convert a string into a float.

Writing the Output File

The function writeFile will write some data into a file:

> writeFile "test.txt" "hello"

This is not a pure function and we will see it again later.

All in One Function

The pure function portion of the exercise will read as the following:

report file = 
	let
		parsed		= map words . lines $ file
		students	= map name parsed
		averages	= map average parsed
		zipped		= zipWith report_line students averages
	in
		unlines zipped

Lecture 17-2

COMP105 Lectures

More Higher Order Functions - Continued

takewhile

This function takes while a condition is true.

> takeWhile (<=5) [1..10]
> [1,2,3,4,5]

This is different to filter which only removes individual elements.

> takeWhile (\ x -> length x <= 2) ["ab","cd","efg"]
> ["ab", "cd"]

Implementation

takeWhile' :: (a -> Bool) -> [a] -> [a]

takeWhile' _ [] = []
takeWhile' f (x:xs)
	| f x = x : takeWhile' f xs
	| otherwise = []

dropWhile

This is the opposite of the previous function:

> dropWhile (==1) [1,1,2,2,3,3]
> [2,2,3,3]

You can also use functions:

> dropWhile (\ x -> x < 10 && x > 0) [1,2,3,10,4,5]
> [10,4,5]

Implementation

dropWhile' :: (A -> Bool) -> [a] -> [a]

dropWhile' _ [] = []
dropWhile' f (x:xs)
	| f x = dropWhile' f xs
	| otherwise = x:xs

takeWhile & dropWhile Example

This function splits up the words in a string into a list of strings containing no spaces.

split_words "" = []
split_words string =
	let
		first = takeWhile (/= ' ') string
		up_to_space = dropWhile (\= ' ') string
		after_space = dropWhile (== ' ') up_to_space
	in
		first : split_words after_space

> split_words "one two		three"
> ["one","two","three"]

words

The function that was made in the last example has a standard implementation called words.

unwords

This is the companion function to words. They aren’t quite inverses as unwords will put spaces inbetween each word.

zipWith

This function zips two lists together using a function:

> zipWith (+) [1,2,3] [4,5,6]
> [5,7,9]

> zipWith (\ x y -> if x then y else -y) [True, False, True] [1,2,3]
> [1,-2,-3]

Implementation

zipWith' :: (a -> b -> c) -> [a] -> [b] -> [c]

zipWith' _ [] _ = []
zipWith' _ _ [] = []
zipWith' f (x:xs) (y:ys) = f x y : zipWith' f xs ys

Examples

mult_by_pos list = zipWith (*) list [0..]

> mult_by_pos [1,2,3]
> [0,2,6]
interleave str1 str2 = 
	let
		zipped = zipWith (\ x y -> [x,y]) str1 str2
	in
		concat zipped

> interleave "abc" "123"
> "a1b2c3"

In this example concat is used to make lists of strings into a single string.

Exercises

  1.  getNumber string = takeWhile (\ x -> elem x ['0'..'9']) string
    
  2.  wordCount string = length $ words string
    
  3.  numberWords string = zip $ words string $ [1..]
    

Lecture 17-1

COMP105 Lectures

More Higher Order Functions

Scan

The scan set of functions is similar to the fold set of functions. However instead of outputting a single value it outputs a list showing the accumulator at each step in the function:

> scanr (+) 0 [1,2,3,4]
> [10,9,7,4,0]

The function scanr is implemented like so:

scanr' :: (a-> b -> b ->) -> b -> [a] -> [b]

scanr' _ init [] = [init]
scanr' f init (x:xs) =
	let
		recursed = scanr' f init xs
		new = f x (head recursed)
	in
		new : recursed

Scan Variants

There are also left to right versions of scan:

> scanl (+) 0 [1..10]
> [0,1,3,6,10,15,21,28,36,45,55]

A good way to understand the accumulator in a fold is to do a scan and observe the output.

Fibonacci Example

fib_pairs n = scanl (\ (a,b _ -> (b,a + b)) (0,1) [1..n]
fib_to_n n = map fst (fib_pairs n)

> fib_pairs 3
> [(0,1),(1,1),(1,2)]

> fib_to_n 3
> [0,1,1]

Exercises

  1.  prefixMaximum list = scanl1 max list
    

    As max is already a two argument function that takes acc and x, then we don’t have to make our own anonymous function.

  2.  powersOfTwo n = scanl (\ acc _ -> 2 * acc) 1 [1..n]
    

Lecture 16-2

COMP105 Lectures

Fold Continued

A fold output can be a different type to the input list.

to_csv list = foldr (\x acc -> show x ++ "," ++ acc)  "" list

> to_csv [1,2,3,4]
> "1,2,3,4,"

By using foldr you cannot get the desired outcome as it applies the same rule to every item in the list.

foldr1

The function foldr1 uses the final value of the list to initialise the accumulator:

foldr1 :: (a -> a -> a) -> [a] -> a

foldr1' _ [] = error "empty list"
foldr1' -[x] = x
foldr1' f (x:xs) = f x (foldr1' f xs)

> foldr1' (+) [1,2,3,4,5]
> 15

As the accumulator has the same type as the list elements foldr1 cannot have a different type to that of the elements in the list.

Example

maximum' list = foldr1 (\ x acc -> if x > acc then x else acc) list

> maximum [1,2,3,4,3,2,1]
> 4

In this example foldr1 is required as your initialisation may become the maximum value.

foldl

foldr processes lists from the right. The opposite of this is foldl which processes lists from the left. For many functions like + the ordering doesn’t matter but for other functions such as /.

Type of foldl

The function f has its type flipped compared to the other fold function:

foldr :: (a -> b -> b) -> b -> [a] -> b
foldl :: (b -> a -> b) -> b -> [a] -> b

This also means that you should swap your functions:

  • foldr (\ x acc -> ...
  • foldr (\ acc x -> ...
Example
reverse_list list = foldl (\ acc x -> x : acc) [] list

> reverse_list [1,2,3,4]
> [4,3,2,1]

Exercises

  1. sumFsts list = foldr (\ (x,_) acc -> x + acc) 0 list
  2. minimum list = foldr1 (\ x acc -> if x < acc then x else acc) list
  3. dash string = foldl (\ acc x -> acc ++ [x, '-']) "" string

Lecture 16-1

COMP105 Lectures

Fold

Some functions take lists and turn them into a single value. This type of computation is called a fold. The things that you can change are:

  • The base case.
  • The operation applied to the list.

foldr

foldr' :: (a -> b -> b) -> b -> [a] -> b

foldr' _ init [] = init
foldr' f init (x:xs) = f x (foldr' f init xs)

> foldr' (+) 0 [1..10]
> 55

> foldr' (*) 1 [1..10]
> 3628800

The Folded Function

sum'' list = foldr (\ x acc -> acc + x) 0 list

The folded function f takes two arguments:

  • x is an element form the list.
  • acc is the accumulator.

The function outputs a new value for the accumulator:

  • The initial value for the accumulator is init.
  • The final value for the accumulator is the output of foldr.
Example
foldr (\ x acc -> acc + x) 0 [1,2,3,4]

Values for the accumulator:

init	= 0
0 + 4	= 4
4 + 3	= 7
7 + 2	= 9
9 + 1	= 10

Final output: 10

An Imperative Equivalent:

foldr f init input_list

In python this would be implemented as:

acc = init
input_list.reverse()

for i in range(len(input_list)):
	acc = f(input_list[i], acc)

return acc

foldr Examples

Whenever you fold a binary operator you are putting that operator in-between each element on the list. If you are using foldr then you will put the initial value on the right hand side.

concat' list = foldr (++) [] list

> concat' ["Hello ", "there."]
> "Hello there."
all' list = foldr (&&) True list

> all' [True, True, True]
> True
length' list = foldr (\ _ acc -> acc + 1) 0 list

> length' [1,2,3,4]
> 4

This example shows that you just need to apply a function to the accumulator for each element in the list. It doesn’t need to be specifically including items in the list:

count_ones list = foldr (\ x acc -> if x == 1 then acc + 1 else acc) 0 list

> count_ones [1,1,2,3,1,4]
> 3

Exercises

  1. any' list = foldr (||) False list
  2. countEs string = foldr (\ x acc -> if x == 'e' then acc + 1 else acc) 0 string
  3. sumOfEvens list = foldr (\ x acc -> if even x then acc + x else acc) 0 list

Lecture 15-2

COMP105 Lectures

filter

filterkeeps only the elements for which f returns True.

filter' :: (a -> Bool) -> [a] -> [a]
filter' _ [] = []
filter' f (x:xs)
	| f x 		= x : rest
	| otherwise	= rest
	where rest 	= filter' f xs
	
filter' even [1..10]
> [2,4,6,8,10]

In comparison to map which applies a function to each element in a list. filter will apply a boolean test to each element in the list and if it fails it will remove the element from the list.

Combining map & filter

square_even :: [Int] -> [Int]
square_even list = map (^2) (filter even list)

square_even [1..10]
> [4,16,36,64,100]

Higher Order Programming

map and filter are examples of higher order programming. This style:

  • De-emphasises recursion.
  • Focuses on applying functions to lists.
  • Are available in imperative languages (Python C++).

There is a whole family of higher order programming functions available in Haskell.

Exercises

  1. onlyDiv3 = filter (\x -> mod x 3 == 0)
  2. onlyLower = filter (\x -> elem x ['a'..'z'])
  3. noEven = map (filter odd)

Lecture 15-1

COMP105 Lectures

map

map applies a funciton f to every element in a list.

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

This saves you from writing out the generic code for applying a function to a list. This allows you to implement the standard functions instead of having to make your own.

map even [1..5]
> [False,True,False,True,False]

This avoids writing a recursive function for applying simple list operations.

Currying and map

It is common to use curried functions with map:

map (*2) [1..5]
> [2,4,6,8,10]

You can also use map to make other functions:

doubleList = map (*2)

> doubleList [1..5]
> [2,4,6,8,10]

Anonymous Functions and Map

It is also common to use an anonymous functions with map:

map (\x -> x * x) [1..5]
> [1, 4, 9, 16, 25]

This lets you apply your own functions to a list without having to define it.

Nested Maps

When working with nested lists, it is common to use nested maps.

map(map (*2)) [[1,2,3],[4,5,6],[7,8]]
> [[2,4,5],[8,10,12],[14,16]]

import Data.Char

map (map toUpper) ["the","quick","brown"]
["THE","QUICK","BROWN"]

This application uses currying in the inner map. It is also especially useful for lists containing strings.

Exercises

  1. cubeList = map (^3)
  2. middleElem = map (\ (_,x,_) -> x)
  3. ruinStrings = map (map (\ x -> if x == 'e' then 'x' else x))

Lecture 14-3

COMP105 Lectures

Anonymous Functions

Sometimes is it convenient to define a function inline. Anonymous functions allow you to use a function without a name.

> (\ x -> x + 1) 7
> 8

This allows you to pass functions to a higher order function without giving it a name:

> apply_twice (\ x -> 2 * x ) 2
> 8

Syntax

The syntax for an anonymous function is:

\ [arg1] [arg2]  ... -> [expression]

The \ is supposed to resemble an lambda $\lambda$.

  • Anonymous function are sometimes called $\lambda$-functions in recognition of lambda calculus.

Functions that Return Functions

Higher order functions can also return other functions:

f_than_adds_n :: Int -> (Int -> Int)
f_that_adds_n n = (\x -> x + n)

> let f = (f_that_adds_n 10) in (f 1)
> 11

This function returns another function that you can use in other functions.

Higher order function can take and return functions:

swap f = \ x y -> f y x

This is a two argument function and then swaps the arguments for the function.

g = swap take

This has swapped the arguments for the function take and can be called with g

Currying Revisited

Previously we’ve seen that it is possible to partially apply a function:

add_two = (+2)

This is just a nicer syntax for a function that returns a function:

add_two = (\ x -> x + 2)

Exercises

  1.  (\ x y -> x + y)
    
  2.  addChar c = (\ string -> string ++ [c])
    
  3.  curry' :: ((a, b) -> c) -> (a -> b -> c)
     curry' f = (\ x y -> f (x, y))
    

Lecture 14-2

COMP105 Lectures

Higher Order Functions

A higher order function is a function that:

  • Takes another function as an argument.
  • Returns a function.
apply_twice :: (a -> a) -> a -> a
apply_twice f input = f (f input)

> apply_twice tail [1,2,3,4]
> [3,4]

This function takes a function and an input as an argument and then uses the supplied function twice on the supplied argument.

An example including currying (partial application):

> apply_twice (drop 2) [1,2,3,4,5]
> [5]

A caveat of using this function is that it must return the same type that is is given.

Function Composition

Function composition applies one function to the output of another

  • Composing f with g input gives f (g input)
compose :: (b -> c) -> (a -> b) -> a -> c
compose f g input = f (g input)

> compose (+1) (*2) 4
> 9

This is the same thing as applying a function to another function.

The . Operator

The type of . is:

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

The . operator is an infix operator called compose. To complete the same action as will the compose function write:

> ((+1) . (*2)) 4
> 9

This means to apply the left function on the argument and then the right function on the result of the previous function.

f list = length (double (drop_evens (tail list)))

f' list = (length . double . drop evens . tail) list

As you can see from the two different implementations; the use of . removes the need for nested brackets, however:

  • It is stylistic.
  • You never need to use .
  • It is preferred.

The $ Operator

evaluate :: (a -> b) -> a -> b
evaluate f input = f input

The $ operator is exactly the same as evaluate. $ is infix.

The $ operator has the lowest precedence of any operator. As a result it can be used in place of brackets:

((*2) . length) [1,2,3,4]
(*2) . length $ [1,2,3,4]

length ([1,2,3] ++ [4,5,6])
length $ [1,2,3] + [4,5,6]

Exercises

  1.  applyThrice :: (a -> a) -> a -> a
     applyThrice f x = f . f . f $ x
    
  2.  f :: (Ord a, Enum a) => [a] -> a
     f x = succ . sum . tail . tail $ x
    

Lecture 14-1

COMP105 Lectures

More Type Classes

show

This function converts other types to strings. However, it can only take the type class of Show into a string:

show :: Show a => a -> a

Show contains:

  • All basic types.
  • All tuples of show-able types.
  • All lists of show-able types.

read

This converts strings to other types, provided they are formatted correctly:

read "123" :: Int

In this case the use of :: is necessary to tell Haskell what type the function should return.

In cases where type inference can be used, the :: is not required:

not (read "False")

The type of read is the type class Read:

read :: Read a => a -> a

Read contains:

  • All basic types.
  • All tuples of readable types.
  • All lists of readable types.

Ordered Types

The type class to compare two objects by inequality is Ord. This is required for using functions such as >.

(>) :: Ord a -> a -> a -> Bool

All basic types can be compared including tuples and lists. Tuples and lists are compared lexicographically (similar to alphabetically ordering words).

Exercises

  1. showTuple :: (Show a, Show b) => (a, b) -> [Char]
  2. addThree :: (Num a, Read a) => String -> a
  3. headLt10 :: (Num a, Ord a) => [a] -> Bool

Lecture 13-2

COMP105 Lectures

Type Classes

Some functions are polymorphic, but can’t be applied to any type. + is a good example. It can work on float and integer but not on mixed types or Char. The type of + is:

(+) :: Num a => a -> a -> a

This means that a is a number in the definition: a -> a -> a.

Num

Num is a type class:

  • It restricts the type variable a to only be number types.
  • Word, Int, Integer, Float, Double are all contained in Num.
  • Char, Bool, tuples and lists are not in Num.

Num Sub-Classes

Num has two sub-classes.

Integral represents whole numbers and contains:

  • Word (un-signed integers)
  • Int
  • Integer

Fractional represents rationals and contains:

  • Float
  • Double
  • Rational

Eq

The types in the class Eq are the types which can be compared with the function ==. There are no default types which aren’t equality testable but if you make your own type then you will have to add it to this class.

Type Class Syntax

The syntax of a class type is:

[Type class 1], [Type class 2], ...) => [Type]

This results in a declaration similar to the following:

equals_two :: (Eq a, Num a) => a -> a -> Bool
equals_two a b = a + b == 2

This type declaration also means that if you use functions like == in your function then you must specify that the type is comparable with Eq a. If you don’t then the compiler will give an error.

The Most General Type Annotation

The most general type annotation is the one that is least restrictive. Ideally you want your type annotation to be the most general without giving an error:

equals_two a b = a + b == 2

-- Too restrictive
equals_two :: Int -> Int -> Bool

-- Most general
equals_two :: (Eq a, Num a) => a -> a -> Bool

-- Too general (will give error)
equals_two :: a -> a -> Bool

The most general type annotation is the one which allows the most valid types

Numbers

In Haskell numbers, such as 10 have a polymorphic type:

10 :: Num p => p

This means that, unless stated explicitly that numbers will be converted to the most suitable type in the class of Num.

You can force a number to be a particular type by using the :: operator:

1 :: Integer
> 1
1 :: Float
> 1.0

The function fromIntegral will take any Integral type and convert it into a Num type. This, for example would allow you to use the / operator on an Integer:

fromIntegral (1 :: Int) / 2
> 0.5

Converting Floats to Integers

Converting floats to integers is a lossy operation. This means that you shoul choose how to complete the rounding:

ceiling 1.6
> 2

floor 1.6
> 1

truncate 1.6
> 1

round 1.6
> 2

Other Type Classes

There are many other type classes in Haskell that won’t be covered:

length :: Foldable t => t a -> Int

This that a should be accepted if it is a list.

If you see any of the following as a types:

  • Functor
  • Foldable
  • Traversable

then think list.

Exercises

  1. square_area :: Num a => a -> a -> a
  2. triangle_area :: Fractional a => a -> a -> a
  3. equal_heads :: Eq a => [a] -> [a] -> Bool

Lecture 13-1

COMP105 Lectures

Polymorphic Types

Some functions accept many different types. An example of this is length which accepts many different types of lists.

if you ask what the type of then function length is then it will return the type:

length :: [a] -> Int

a is a type variable. This means that:

  • The function can be applied to any list.
  • a will represent the type of list elements.

This type is called polymorphism. This is a feature of functional languages.

Type Variables

In the function head it will return the type variable which it was given in the list. Hence asking for the type returns:

head :: [a] -> a

Type variables can appear more than once. These types specify that the return type will be determined by the type of the input.

Multiple type variables can be included in one declaration for example the function fst returns the following:

fst :: (a,b) -> a

As a result the types of function can tell you a lot about what the function does.

Type Annotations

Is it good practice to insert type annotations for your functions. This is so that Haskell assigns the types to your functions that you expect. You do this with the following syntax.

f :: Int -> Int
f x = x + 1

The annotation is usually place before the function definition. If you don’t give a type annotation, then Haskell will infer one for you based on the functions used in your function. This is a result of the language being strongly typed.

Annotating your function can make it easier to catch bugs. This will give the compiler more to work with and make errors more descriptive.

Exercises

  1. take :: Int -> [a] -> [a]
  2. (:) :: a -> [a] -> [a]
  3. (++) :: [a] -> [a] -> [a]

Lecture 12-2

COMP105 Lectures

Function Types

Functions also have types in the type system. The type of a function is based on the values it takes as an argument and returns.

Single Argument Functions

For a one argument function, the type is written as:

[input type] -> [output type]

Multi-Argument Functions

For a function with more than one argument, the type uses multiple ->:

[input type] -> [input type] -> [output type]

This makes sense as a Haskell function can only return a single type.

Partial Application

In most functional languages, function can be partially applied. This means that you call a function with fewer arguments than are required.

plus a b = a + b

plus2 = plus 2

> plus2 10
> 12

> plus2 1
> 3

As you can see currying a function is the same as applying a permanent input to a function and leaving the other one open. This means that:

f = plus 1

is the same as:

f x = plus 1 x

In partial application:

  • We fix some of the arguments.
  • We leave other arguments unfixed.

This creates a new function that only has the unfixed arguments.

Partial Application Types

As the function made in a partial application just a function that takes the unfixed arguments, the inputs are the types of the unfixed arguments. For the functions:

func a b c = a + b + c

func2 = func 23

The type would be func2 :: Int -> Int -> Int, as the unused arguments are carried over.

Argument Order

The order of the partial application must follow the same order as the original function. This means that is makes more sense to use it on prefix functions:

f = (:) 1

This would allow you to cons 1 onto another list.

Partially Applying Infix Operators

To partially apply an infix operator see the following example:

f = (/2)

This will take a single input and divide it by two. As you can see the infix operator should be put in brackets.

Bracketing for Function Types

Function application should be thought of multiple partial applications.

((multThree x y z = x) * y) * z

This means that the function type brackets to the right:

Int -> Int -> Int -> Int

is the same as:

Int -> (Int -> (Int -> Int))

This is a first look at a higher order function. This means that we are writing one argument functions that return other functions.

Multiple Arguments v.s. Tuples

Previously we’ve seen that you can write function in two ways:

  • Using usual “spaces” syntax.
  • Using tuples.
multThree x y z = x * y * z
multThree' (x, y, z) = x * y * z

These both do the same thing, but the second version cannot be partially applied. This means is is best to avoid tuples unless they are necessary.

Exercises

  1. isA :: Char -> Bool
  2. isADouble :: Char -> Char -> (Bool, Bool)
  3. exclaim :: [Char] -> [Char]

Lecture 12-1

COMP105 Lectures

Types

Everything in Haskell has a type. In GHCI :type or :t will display the type of an expression.

Basic Types

Int

Holds a 64-bit integer between $-2^{63}$ and $2^{63}-1$

Integer

Holds arbitrary size integers but is slightly slower than an Int. Ideally you should use an Int if you are using smaller numbers.

Float

Holds 32-bit floating point numbers.

Double

Holds a 64-bit floating point number.

Bool

Holds truth values.

Char

Holds a single character and can store any Unicode character.

Compound Types

Tuples

The type of a tuple is the type of its constituents.

  • The size of a tuple is encoded in its type.
  • Tuple elements can be different types.

Lists

The type of a list is determined by the type of its elements.

  • A list of typex is denoted by [x].
  • This is why lists must contain elements of the same type.
  • The length is not encoded in the type.

Exercises

  1. :: [Bool]
  2. :: ([[Bool]], [Char])
    • As strings are just lists of Char then you must represent them as such.
  3. :: [([Bool], Bool)]

Lecture 10-2

COMP105 Lectures

Cracking the Caesar Cipher

In order to crack a Caesar Cipher you can use the fact that, in English, each letter isn’t used equally. From this you can shift around the letters until you find a shift that fits the distribution of the English language.

As a result of this you should be able to write a program that can guess and decode an English Caesar Shift.

Chi-Squared Score

\[\sum^z_{i=a}{\frac{(\text{freq}_i-\text{english}_i)^2}{\text{english}_i}}\]

To check if two frequency distributions are similar you can use the chi-squared score. The lower the output the closer the distributions match.

The algorithm will complete the following tasks:

  • Try all 26 possible shifts
  • For each one, compute the letter frequency distribution of the decoded text, and the chi-squared score.
  • Use the shift with the lowest chi-squared score.

Implementation

View the slides and the source code for the implementation and explanation.

Lecture 10-1

COMP105 Lectures

The Caesar Cipher

The Caesar Cipher is a shift method of encoding. This shifts every letter forward by three and wraps around when z is reached.

There is no particular reason to shift by three and you can shift by any number between zero and 25. Shifting by 13 would be a ROT13.

Working with Characters

The Data.Char package has some useful functions for working with characters.

  • The ord function takes a character and returns its ASCII value.
  • The chr function turns an ASCII value into a character.

To import a package use the line:

import ...

using the name of the package you want to import.

Implementation

View the slides and the source code to find the implementation of the Caesar Shift functions.

Lecture 9-3

COMP105 Lectures

Mutual and Multiple Recursion

Mutual Recursion

Mutual recursion is when two functions call each other:

even' 0 = True
even' n = odd' (n-1)

odd' 0 = False
odd' n = even' (n-1)

We have to make sure that:

  • We terminate in a base case.
  • We always make progress towards a base case.

Mutual recursion is a stylistic choice.

Multiple Recursion

Multiple recursion is when a function makes more than one recursive call in the same recursive rule. This has been see in the fib function that we made.

Multiple recursion can make your code slow as each call makes two additional threads growing the recursive tree exponentially. Additionally the same function is called multiple times with the same parameters.

graph TD
A[fib 4] --> B[fib 3]
A --> C[fib 2]
B --> D[fib 2]
B --> E[fib 1]
D --> F[fib 1]
D --> G[fib 0]
C --> H[fib 1]
C --> I[fib 0]

Faster fib

Create a helper function that computes the Fibonacci list:

fast_fib_help 1 = [1,0]
fast_fib_help n = x + y : (x:y:xs)
	where (x:y:xs) = fast_fib_help (n-1)

This returns the first n Fibonacci numbers counting down to 0. To turn this into a more presentable list we can use another user facing function:

fast_fib n = head (fast_fib_help n)

This function is not multiply recursive and is therefore must faster to compute.

Exercises

  1.  multipleThree 0 = True
     multipleThree x = two (n-1)
    	
     one 0 = False
     one x = multipleThree (n-1)
    	
     two 0 = False
     two x = one (n-1)
    
  2.  lucas 0 = 2
     lucas 1 = 1
     lucas n = lucas (n-1) + lucas (n-2)
    

Lecture 9-2

COMP105 Lectures

Recursion with Multiple Lists

Sometimes we want to use recursion on more than one list at a time. The following example adds the elements of two lists together:

add_lists _ [] = []
add_lists [] _ = []
addlists (x:xs) (y:ys) = x + y : addlists xs ys
  • Base cases stop when either of the lists is empty.
  • Recursive rule pulls an element form both lists.

Another method to implement this, provided that you don’t want to accept lists which don’t have equal length is like the following:

f [] [] = []
f [] _ = error "Second list is longer"
f _ [] = error "First list is longer"
f (x:xs) (y:ys) = x + y : f xs ys
  • This will give an explicit error if they aren’t matching length.

Splitting a List in Two

Other functions can take a list and return a pair of lists:

gt_10 [] = ([],[])
gt_10 (x:xs)
	| x > 10	= (x:gt, lt)
	| otherwise	= (gt, x:lt)
	where (gt, lt) = gt_10 xs

This will split a list into two lists containing values that are bigger and smaller than 10.

  • The base case sets up the tuple.
  • The recursive rule modifies one of the two lists.

zip

zip takes two lists and returns a list of pairs

zip' [] _ = []
zip' _ [] = []
zip' (x:xs) (y:ys) = (x, y) : zip' xs ys

Exercises

  1.  multiplyLists _ [] = []
     multiplyLists [] _ = []
     multiplyLists (x:xs) (y:ys) = x * y : multiplyLists xs ys
    
  2.  zip3' [] _ _ = []
     zip3' _ [] _ = []
     zip3' _ _ [] = []
     zip3' (x:xs) (y:ys) (z:zs) = (x,y,z) : zip3' xs ys zs
    

Lecture 9-1

COMP105 Lectures

where Syntax

Sometimes it is useful to bind names across a whole function:

remove_twos [] = []
remove_twos (x:xs)
	| x == 2	= rest
	| otherwise = x:rest
	where rest = remove_twos xs

As the two expressions are in different guards then you can’t use a let expression.

A general example is:

f x y = 1 + a + b
	where 	a = 2 * x
			b = y / 2

where comes at the end of a function. Like let it can bind any number of names

  • These names are available throughout the function.
  • They are not variables.
  • You can do pattern matching in where clauses (and also in let expressions too).

An example of pattern matching:

initials first last = (x,y)
	where	(x:_) = first
			(y:_) = last

let v.s. where

let is an expression.

> (let a = 1 in a) + (let a = 2 in a)
> 3

where is syntax

  • One where clause per function.
  • Particularly useful when used with guards.
remove_twos [] = []
remove_twos (x:xs)
	| x == 2	= rest
	| otherwise = x:rest
	where rest = remove_twos xs	

This means that where has to be joined to a function body and is always bound to that function body.

Exercises

  1.  func x = a + b + x
         where 	a = 2 * x + 1
                 b = a * a - x
    
  2.  countTwos [] 	= 0
     countTwos (x:xs)
         | x == 2	= 1 + rest
         | otherwise = rest
         where rest = countTwos xs
    

Lecture 8-2

COMP105 Lectures

List Recursion Examples

In this section we will be re-implementing the built in functions for operating on lists.

take

take' 0 list	= []
take' n []		= []
take' n (x:xs)	= x : take' (n - 1) xs

With the two base cases the program will end in one of two ways:

  • We take as many items as we asked for.
  • We run out of elements to take.

drop

drop' 0 list	= list
drop' n []		= [] 	-- could also be an error
drop' n (x:xs)	= drop' (n - 1) xs

elem

elem' e [] = False
elem' e (x:xs)
	| e == x = True
	| otherwise = elem' e xs

This will cycle through every item in the list and will stop and say True if it is found. If it isn’t found then it will return False.

This also implements pattern matching and guards in one function.

maximum

Maximum will say which is the largest element in a list.

maximum' []	= error "Called with empty list." 
-- This prints an error with this label
maximum' [x] = x 
-- This pattern matches against a list of length 1.
maximum' (x:xs) =
	let
		max_tail = maximum' xs
	in
		if (x > max_tail) then x else max_tail

This checks from the back of the list and keeps the largest element in the list as x

reverse

reverse' []		= []
reverse' (x:xs) = reverse' xs ++ [x]
-- ++ puts an element onto the tail of a list

Consuming More Than One Element

add_adjacent

add_adjacent []			= []
add_adjacent [x]		= error "Odd number of elements"
add_adjacent (x:y:xs)	= x + y : add_adjacent xs
> add_adjacent [1,2,3,4,5,6]
> [3,7,11]

add_next

add_next [] 		= error "Not enough elements"
add_next [_] 		= error "Not enough elements"
add_next [x,y]		= [x+y]
add_next (x:y:xs)	= x + y : add_next (y:xs)

Exercises

  1.  containsThree [] = False
     containsThree (x:xs)
         | x == 3	= True
         | otherwise	= containsThree xs
    
  2.  sumEvenIdx []		= 0
     sumEvenIdx [x]		= x
     -- Required to catch 1 and odd element lists.
     sumEvenIdx (x:_:xs)	= x + sumEvenIdx xs
    

Lecture 8-1

COMP105 Lectures

List Recursion

Lists have a head and a tail. We use these command and pattern matching to recurse lists. Generally list recursions use the empty list [] as part of the base case.

Consuming Lists

sum' []		= 0 
sum' (x:xs)	= x + sum' xs
  • The base case is the empty list.
  • The recursive rule breaks the list into its head and tail.
length' []		= 0
length' (_:xs)	= 1 + length' xs
  • This time the head of the list isn’t used.

Building Lists

We can also build lists using recursion.

down_from 0	= []
down_from x	= x : down_from (x-1)

This will produce a list counting down from the number provided until zero is reached.

Transforming Lists

If you want to consume and build a list in one go it is called transforming a list.

square_list []		= []
square_list (x:xs)	= x * x : square_list xs

This list will square each value and add it recursively onto the list.

The defining feature of transforming lists is that the base case has an input of [] which is equal to the output of [].

Exercises

  1.  product' []		= 1
     product' (x:xs)	= x * product' xs
    
  2.  upToTen 10	= 10
     upToTen x 	= x : upToTen (x + 1)
    
  3.  halveList [] 		= []
     halveList (x:xs) 	= (x / 2) : halveList (xs)
    

Lecture 7-2

COMP105 Lectures

More Complex Recursion and Guards

Multiple Base Cases

Each base case represents a different stopping position:

isEven 0 = True
isEven 1 = False
isEven n = isEven (n - 2)
fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n = fibonacci (n - 1) + fibonacci (n - 2)

Multiple Recursive Rules

More complex recursive functions may have more than one recursive rule.

evenSum 0 = 0
evenSum 1 = 0
evenSum x = if x `mod` 2 == 0
			then x + evenSum (x - 1)
			else evenSum (x - 1)

This function adds the even numbers less than the number provided. Hence it only adds x into the sum when it is even and if not it goes onto the next value.

It is important to make sure that the rules are comprehensive and that no value will crash the program.

Guards

This is the same function as before but using guards:

evenSum x
	| (x == 0) = 0
	| (x == 1) = 0
	| (x `mod` 2 == 0) = x + evenSum (x - 1)
	| otherwise = evenSum (x - 1)

To use guards, you write your function name and parameters. On the next line, with a pipe, you can write your test and return value:

func x
	| x < 5		= "less than five"
	| x > 6		= "bigger than five"
	| x == 5	= "equal to five"

They are parsed from top to bottom and should take into account all cases as the interpreter won’t check for completeness. The word otherwise will cover all edge cases and make any partial function total.

Guards vs. Pattern Matching

  • For tests of just equality the best way is to use pattern matching.
  • Otherwise guards are to be used as it will save lines written.

Advice on Recursion

  • When do you want to stop?
    • These are the base cases.
  • How do you make progress towards a base case?
    • How do you make the problem smaller.
    • There might be multiple cases to consider.
      • These will be the recursive rules.
  • What do you need to do to get to the smaller case?
    • These will be the operations that you need to carry out within each rule.

Exercises

  1.  sign x
         | x < 0		= "negative"
         | x == 0	= "zero"
         | x > 0 	= "positive"
    
  2.  odd_product x
         | x == 1		= 1
         | mod x 2 == 0	= odd_product (x - 1)
         | otherwise		= x * odd_product (x - 1)
    

Lecture 7-1

COMP105 Lectures

Recursion

There is no such thing as a while loop in Haskell so you must use recursion to complete the same task. This links into Assignment 1 as it leans heavily on recursion.

Factorial

A recursive function is one that calls itself:

factorial n = if n > 1
				then n * factorial (n - 1)
				else 1

A recursive function has:

  • A base case.
    • The else case is the base case and is the state where recursion stops.
  • One or more recursive rules that move us closer to the base case. When the base case is reached then the program will terminate.
    • The rule is calling ourself with n - 1
      • This makes progress to the base case as you are making n smaller and getting closer to 0.

Result of Factorial

factorial 4
> 4 * factorial 3
> 4 * 3 * factorial 2
> 4 * 3 * 2 * factorial 1
> 4 * 3 * 2 * 1
> 24

A good way to debug smaller iterations is to expand and follow the iterations to find the issue.

Factorial Using Pattern Matching

You can use pattern matching to remove the if in a recursive function:

factorial 1 = 1
factorial n = n * factorial (n - 1)

This is possible as Haskell processes functions from top to bottom. This means that first it will check if the argument matches 1 and if not fall through to the next case.

Another simple example:

f 1 = 1
f 3 = 2
f x = 0

If the pattern matching isn’t exhaustive then you may get an error if an unmapped input is supplied. An exhaustive function maps all inputs.

This style of pattern matching is similar to a case select.

Base Cases

Every function must have a base case:

  • It gives a stopping condition for the recursion.
  • It is usually the simplest case.
  • You can have more than one base case.

Recursion with no base case will never terminate.

Comparison to Imperative Languages

while (condition)
{
	<lots of computation>
}
  • Base cases are like the stopping condition.
  • Recursive rules do the computation.
  • Anything you can do in a loop can be done by recursion but there is no simple way to translate between the two.

Exercises

  1. smallPrime 2 = True
    smallPrime 3 = True
    smallPrime 5 = True
    smallPrime 7 = True
    smallPrime x = Fase
    
  2.  sumUpTo 1 = 1
     sumUpTo n = n + sumUpTo(n - 1)
    

Lecture 6-2

COMP105 Lectures

List Comprehensions

List ranges can produce simple arithmetic sequences but list comprehensions can produce more complex lists:

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

By using this you can apply a function to a list and put the result into a list.

Predicates

Predicates also add evaluations into the list to limit the results.

> [x*x | x <- 1..10, x*x > 40]
> [49,64,81,100]

This limited the output of the previous example to be strictly greater than 40.

Multiple predicates can be separated by multiple commas:

> [x*x | x <- 1..10, x*x > 40, x*x < 80]
> [49,64]

In Functions

The body of a function can be a list comprehension:

-- This function will print the even values less than a supplied value.

evens_less_than y = [x | x <- [0..(y - 1)], x `mod` 2 == 0]

To test each value in a list the following can be applied:

-- This function will step through each value in the list `xs`
-- and apply the function.

lts10 xs = [ if x  < 10 then "Yes" else "No" | x <- xs]

Multiple Variables

You can also use more than one sublist in a list comprehension. This will step though every combination of the two lists and print the operation of the lists. This is similar to a nested for loop.

> [ x*y | x <- [2,5,10], y <- [8,10,11]]
> [16,20,22,40,50,55,80,100,110]

This can also be combined with predicates:

> [ x*y | x <- [2,5,10], y <- [8,10,11], x*y > 50]
> [55,80,100,110]

Look at the slides for more examples of the topics above. This includes examples for some non-trivial functions such as: Factors of a Value, Prime Numbers up to a Value.

Lists of Lists

  • Lists can contain lists provided that they contain the same type. These lists don’t merge but each element in the main list is the sublists:

      [[1,2,3,4],[1,2,3,4]]
    
  • Tuples can also be put in a list provided that they are the same type. This means that the tuples must be:

    • Containing the same type.
    • Of the same length.
      [(1,2,3,4),(1,2,3,4)]
    
  • As a result of the lists containing a list the following happen:

      > let x = [[1,2,3],[4],[5,6]]
    
      > head x
      > [1,2,3]
    
      > tail x
      > [[4],[5,6]]
    
      > length x
      > 3
    
  • As you can have lists of lists you can also nest list comprehensions in lists. An example of this is in the slides.

List Comprehensions in Other Languages

List comprehensions arose in the functional programming world but they have appeared in imperative languages.

For example in Python:

squares = [x**2 for x in range(10)]

[x.lower() for x in ["A","B","C"]]

Exercises

  1.  cubesupto x = [y * y * y | y <- [1..x]]
    
  2.  nospaces xs = [x | x <- xs, x /= ' ']
    
  3.  allpairs x y = [(a,b) | a <- [1..x], b <- [1..y]]
    

Lecture 6-1

COMP105 Lectures

List Ranges

A list range lets us write a long list in a compact way:

> [1..10]
> [1,2,3,4,5,6,7,8,9,10]

If the start is bigger than the end then you will get an empty list. This means you can’t count down in a list in this way. Using .. also works with letters to fill in the letters in between.

To give a step size give the first two elements and then a number to stop before or on:

> [3,6..20]
> [3,6,8,12,15,18]

To count backwards use the a step size of -1:

> [5,4..2]
> [5,4,3,2]

Infinite Lists & Functions

  • You can also use this notation to define an infinite list:

      > [1..]
      > [1,2,3,4,5...
    

    This will last forever and you can pass then on to functions. This may be useful if you want some number of elements in an infinite list take 3 [1..].

  • To make a list of an an infinitely long value you can use repeat.

      > repeat 'a'
    
  • To make a list of an infinite cycle you can use the cycle function.

      > cycle "abc"
    

Exercises

  1.  onetox x = [1 .. x]
    
  2.  evensupto x = [0, 2 .. x]
    
  3.  countdown x = [x, x - 1 .. 0]
    

Lecture 5-2

COMP105 Lectures

Lists

A list contains any number of items that all have the same type.

[1, 2, 3, 4, 5]
['a', 'b', 'c', 'd', 'e']

Lists are built with [] as opposed to tuples which are built with().

  • A list can have any number of elements including 0.

      []
      [1]
      [1, 2]
    
  • In Haskell lists are linked lists:

    • This means that random access is expensive.
    • Internally Haskell will walk the entire list to get to the last element.

Strings

In Haskell strings are just a list of characters. This means that ['a', 'b', 'c'] is equivalent to "abc". This has the result of allowing any string operator to also work on lists.

Operators

  • You can join lists together with the ++ operator, provided that they have the same type.
  • The !! operator gets a specified element from the list.
    • Lists are zero indexed.

        > [1 ,2 ,3] !! 0
        > 1
      
  • head takes the first element of a list.
  • tail takes all but the first element of the list.
  • : or the cons operator glues a new head onto an existing list.

      > 1 : [2, 3, 4, 5]
      > [1, 2, 3, 4, 5]
    
    • Any list can be built by consing onto an empty list.
      > 'a' : 'b' : 'c' : []
      > "abc"
    
  • last gives the last element of the list.
  • init gives all elements but the last element.
  • length returns the number of elements in the list

See the slides for more functions.

Pattern Matching Example

triple_head (x:xs) = 3 * x

This will name the head of the list x and the tail xs as the input is x consed onto xs.

  • It is common for the head to be called x or another value and for the tail to be called xs with an s on the tail.
  • If the pattern cannot be matched, such as if the list is empty, then you will get an error.
  • The wildcard pattern _ won’t assign a letter if you don’t care about a value.

      f (_:y:_) = 3 * y
    

Exercises

  1.  thricesum list = sum list * 3
    
  2.  thirdelement _:_:x:_ = x
    
  3.  exclaim string = '!':string:'!'
    

Lecture 5-1

COMP105 Lectures

Tuples

A tuples allows us to bind two or more values together:

(1,2)
("A", "few", "words")
(6, "six")
  • Tuples can have any size but must be at least 2.
  • The size should be thought of as being fixed as it is not easy to change the length of a tuple.
  • Tuples can mix types.

Example Functions

Taking Tuples as Input

f (x, y) = x

This will take a tuple with the elements x and y and will return the first element of the tuple.

Returning Tuples as Output

f x y = (max x y, min x y)

Syntax

f x y = x + y
g (x, y) = x + y

Both will give the same output but the recommendation is to use the Haskell method instead of always passing tuples.

Exercises

  1.  exercise1 x = (x / 10, 100 * x)
    
  2.  exercise2 (x, y, z) = x + y + z
    
  3.  exercise3 (x, y, z) = (z, y, x)
    

Lecture 4-2

COMP105 Lectures

Let

Sometimes we want to use the same expression more than once:

(x * x - 4) + sqrt (x * x - 4)

The problem of writing out the same equation over again can be solved using the following example:

let s = (x * x - 4) in s + sqrt s

Syntax

let <bindings> in <expression>

Where:

  • <binding> gives names to bindings separated by ;
    • let a = 1; b = a + 1 in a + b
  • <expression> uses those bindings

Let vs Variables

A let expression doesn’t create variables:

  • They are names for particular expressions.
  • You cannot change a binding once it has been made.
  • They are made for convenience only.

Let in GHCI

In GHCI you can write:

> let a = 1

or

> a = 1

To define a let for the rest of the session.

Let across multiple lines

Usually it is clearer to write let across multiple lines:

f x y = let a = x * x
			b = y * y
		in
			a * a + b * b

When splitting across line breaks you don’t need to use ; to separate the bindings.

Scope

When using let the bindings are only defined within the let function.

Examples

cylinder r h =
	let sideArea = 2 * pi * r * h
		topArea = pi * e ** 2
	in sideArea + 2 * topArea

Exercises

  1.  exercise1 x = let a = x * x + 1 in a * a * a
    
  2.  exercise2 x = let a = x + 1; bb = a + 2; ccc = a + bb in ccc *  ccc
    

Haskell’s Layout Rule

Each definition at the same level should start on exactly the same column:

f x y z = let
			a = x * x
			b = y * y 
			cc = z * z
		in
			a * b + b * b

Ignoring the Layout Rule

You can ignore the layout rule by using curly braces to separate the bindings.

let {a = x * x;
	b = y * y;
	c = z * z}
in
	a + b

Lecture 4-1

COMP105 Lectures

IF

Differences Between Imperative and Functional

In imperative languages if changes the control flow but in functional languages there is no control flow.

Functional if gives a value that you will return based on a test.

if (1==1) then "yes" else "no"

Rather than controlling flow, functional if chooses between two alternatives and is a pure function.

f x = (if x > 10 then 1 else 0) + 2

The functional if is more commonly known as the ternary operator as it has three arguments.

Things of Note

  • For a functional if both branches much always be present.
    • A pure function must always return a value.
  • Both branches must have the same type.
    • This is because Haskell is strongly typed meaning that a single function can only return values of the same type.
  • Nested ifs are not recommended and there are better ways to complete the same task.

Structure of an IF

if A then B else C

A, B or C may be anything that can evaluate to a single value. This can be a value itself or another function.

Exercises

  1.  between36 x = if (x > 3 && x < 6) then "yes" else "no"
    
  2.  min' x y = if x < y then x else y
    
  3.  max3 x y z = if (x > y && x > z) then x else (if y > z then y else z)
    

Lecture 3-2

COMP105 Lectures

Defining Custom Functions

Functions are defined as such:

[functionName][arguments][=][body]

Functions and arguments must start with small letters as only types use capitals.

They can be written into a file and loaded into GHCI or compiled for use in a program. An example of a simple function:

addTwo x = x + 2
twoInAddTwo x y = x + y + 2

Additional functions are available in the slides.

Loading Functions

  • To load a functions from a file run GHCI on the file. ghci functions.hs
  • To reload the current file run :reload in GHCI.
  • To load in a file run :load and the file-path.

Comments

Single line comments can be written -- like this

Multi-line comments can be written:

{- Like
   this-}

Compilation

Instead of running code in the interpreter you can compile it using GHC. To print the output to the StdOut you can use the two functions putStrln(show()) to convert the output to a string and print that to the StdOut.

This is part of the IO function-set and won’t be used again for a while.

Exercises

  1.  double x = 2 * x
    
  2.  pythagoras a b  = sqrt (a ** 2 + b ** 2)
    
    • Must use the float exponentiation operator to allow for floats as a or b.
  3.  maxFour a b c d = max (max a b) (max c d)`
    

Lecture 3-1

COMP105 Lectures

Functions and Libraries

When using Haskell the default library is Prelude.

Haskell uses special syntax for function calls. min from the Prelude library will work as min 1 2. This is as functions are called as:

[function name][space][arg1][space][arg2]...

Additionally in Haskell functions bind tighter than mathematical operators:

Prelude> min 28 100/4
7.0
Prelude> min 28 (100/4)
25.0

Two Argument Function Syntax

Functions will two arguments can be infixed by surrounding with back-ticks:

Prelude> mod 10 4
2
Prelude> 10 `mod` 4
2

For infix functions like + you can surround them with brackets and use them like a prefix function:

Prelude> 1 + 1
2
Prelude> (+) 1 1
2

Additional functions are available in the slides.

Lecture 2-2

COMP105 Lectures

What is Functional Programming?

In a functional programming language everything is a pure function.

  • The program is built out of pure functions.
  • Simple functions are combined to build more complex functions.

This very different style still allows you to do anything you could have done in an imperative language.

Building Functions

Every line is built in the form of:

f(x) = <some other function>

Functions are built up from other functions:

f(x) = square(x) + x
g(x) = h(i(x), j(x))

A pure functional program is similar to an imperative language where each subroutine only has one line and immediately returns a value.

What isn’t in Functional Programming?

  • Functional programming has no concept of a variable as variables rely on side effects to operate.
  • Functional Programming doesn’t allow loops. This is because loops need variables to operate. Recursion is used instead.
    • Anything you can do with a loop can also be done with recursion.
  • There is no notion of control flow as everything is just function application.
    • Control flow is the notion that instructions are followed one at a time in a list.

Functional programming is about passing around your answers.

Lecture 2-1

COMP105 Lectures

What is a pure function?

A function takes inputs and produces outputs. E.g. Input: $x$, Output: $f(x)$

In imperative languages, functions can do much more and are called subroutines.

Every function can be implemented in a subroutine but not all subroutines are functions.

A function maps inputs to outputs, however subroutines can have an effect on the global state. The global state is anything that is not within the function, such as modifying global variables, printing, network access…

Pure functions only influence the outside world through the return value.

When does this matter?

When the compiler compiles the code it may want to change the order of the instructions:

y = f(1) + f(2)  
y = f(2) + f(1)

For a function this will work but for a subroutine it may write out globally and not produce the desired effect.

Using functions makes compiler optimisations, code refactoring, and parallelization easier as the functions can be run in different orders, or concurrently at runtime.

More Rules

Pure functions always return a value

This is because pure functions only interact via their return value. If they don’t have a return value they have no effect.

Pure functions must be deterministic.

This means that they must return the same value every time, provided that they have the same input.

Determinism allows for logical assumptions such as f(x) + f(x) == 2 * f(x). If the function was a subroutine and returned a random value this wouldn’t be the case.

Summary

  • Pure Functions
    • Are a black box
    • Have no side effects
    • Are deterministic

Every pure function is a subroutine, some subroutines are not pure functions.

Lecture 1-2

COMP105 Lectures

Covering dates and logistics of homework and lectures.

Learning Outcomes

  1. Describe functional and imperative languages and the differences between them.
    • Weeks 1 - 2
  2. Apply recursion to solve algorithmic tasks.
    • Weeks 3 - 4
  3. Apply common functional programming idioms such as map, filter, fold and scan.
    • Weeks 5 - 8
  4. Write programs using a functional programming language.
    • Weeks 9 - 10

Assessments

  • Three programming assignments
    • Assignment 1 - Recursion - 20%
      • Week 4, deadline week 6
    • Assignment 2 - Functional Programming idioms - 20%
      • Week 7, deadline week 9
    • Assignment 3 Write a full program - 25%
      • Week 10, deadline week 12
  • One class test - 25%
    • Week 10, deadline week 12
  • Weekly homework sheets - 10%

Lecture 1-1

COMP105 Lectures

Programming languages can be split into imperative and functional. This course will focus on the functional language of Haskell.

  • Imperative programs tell the computer how to compute the answer.
    • Declare variables
    • Go around a loop
    • Do the same instructions each time
  • Functional programming languages follow mathematic definitions and focus on recursion. No variables are declared and no explicit loops.
    • No variables
      • No such thing as a variable in functional programming.
    • No explicit loops
      • using recursive functions.

Functional programming is a style of programming the isn’t dependant on the language that is is written in. Functional programming languages are built to support this style.

Course Focus

The course will focus on functional languages but we will compare the two styles of programming.

Haskell is a pure functional languages as you cannot program in an imperative style easily.

Why Functional Languages are Important

  1. Their usefulness is increasing
    • Multi-core systems and GPUs prefer highly parallel code which functional programs are.
  2. Learning the functional style can make you a better imperative programmer.
    • Sometimes the functional style is more appropriate.
    • Many imperative languages support functional styles.
  3. Functional programming is a good preparation fo a computer science education.
    • Algorithms in CS are often presented in a functional way.
    • Functional programming helps you translate the algorithms into functional code.
    • The functional paradigm is also used in the analysis of algorithms.