2-FuncSyntax
2-FuncSyntax
Yuepeng Wang
Spring 2025
1
Overview
• Pattern matching
• Conditionals
• List comprehensions
• Recursion
• Mutual recursion
• Tail recursion
2
Pattern Matching
• Pattern matching specifies patterns to which the data should conform and
to deconstruct the data according to those patterns
• Intuitively, we can express “if the argument looks like this, the result is …”
3
Pattern Matching: Example
• A function for logical and
myAnd :: Bool -> Bool -> Bool
myAnd True b = b
myAnd False _ = False
• First line of the implementation says: if the first argument is True, the result
is the same as the second argument
• The named argument (e.g., b) matches anything and captures its value
• The literal (e.g., True, False) matches the value when equal
5
Pattern Matching: Order
• The order of different patterns is very important
• Rule: the patterns will be checked from top to bottom. The first pattern
that matches the input applies
6
Pattern Matching: Tuples
• Pattern matching can also be used on tuples
• We might write the following code for pair addition without pattern
matching (fst and snd are functions from Prelude)
addPairs :: (Int, Int) -> (Int, Int) -> (Int, Int)
addPairs x y = (fst x + fst y, snd x + snd y)
7
Pattern Matching: Lists
• We can also pattern match against lists
• Execution results
ghci> myHead [1, 2, 3]
1
ghci> myHead []
*** Exception: Empty List
8
Pattern Matching: Lists
• This pattern is very common for recursing over a list
• Code template
func [] = …
9
Patterns of Lists
• [ ] matches an empty list
List x y xs
[1,2,3,4,5] 1 2 [3,4,5]
[1,2] 1 2 []
11
Pattern Matching: As-Patterns
• As-patterns can break up a value according to the pattern, while still
keeping a reference to the original value
12
Non-Exhaustive Patterns
• If the input is not matched by any pattern, Haskell throws an exception
"non-exhaustive patterns"
• Example
badAdd :: [Int] -> Int
badAdd (x:y:_) = x + y
• It only adds the first two elements for lists with length >= 2
ghci> badAdd [1,2,3,4]
3
ghci> badAdd [1]
*** Exception: Non-exhaustive patterns in function badAdd
13
Non-Exhaustive Patterns
• "Non-exhaustive patterns" typically means that the programmer misses
some cases of the input in the implementation
14
Conditionals: If-Then-Else
• Haskell has several different ways to write code involving conditions
• Example
pos :: Int -> String
pos x = if x > 0
then "Pos"
else "Non-Pos"
15
Conditionals: Guarded Equations
• An alternative way to avoid scary nested if-then-else: guarded equations
mySignum :: (Num a, Ord a) => a -> Int
mySignum x
| x>0 = 1
| x<0 = -1
| otherwise = 0
{
signum(x) = −1 if x < 0
0 otherwise
16
Conditionals: Case Expressions
• Case expressions can be used to define results based on the possible
cases of a value
word :: Int -> String
word n = case n of
1 -> "one"
2 -> "two"
3 -> “three"
_ -> "something else"
17
Conditionals: Case Expressions
• All cases of a case expression are patterns, just like function definitions
describeList :: [a] -> String
describeList ls = "The list is " ++ case ls of
[] -> "empty"
[_] -> "a singleton list"
_ -> "a longer list"
• Execution results
ghci> describeList [1,2,3,4,5]
"The list is a longer list"
ghci> describeList [1]
"The list is a singleton list"
ghci> describeList []
"The list is empty”
18
List Comprehensions
• Recall the notation from math for building sets
2
{x | x ∈ {1,…,5}, even x}
19
List Comprehensions: Generators
• The generator creates source values in the comprehension
• A generator uses the drawn from operator <- with a list on the right
20
List Comprehensions: Generators
• A list comprehension can have more than one generator, with successive
generators being separated by lemmas
• Example
ghci> [ 10*x + y | x <- [1, 2, 3], y <- [4, 5] ]
[14, 15, 24, 25, 34, 35]
21
List Comprehensions: Guards
• List comprehensions can also use logical expressions called guards to
filter the values produced by generators
22
List Comprehensions
• The .. operator can be used to build a list/sequence
Expression Result
[1..5] [1,2,3,4,5]
[3,6..20] [3,6,9,12,15,18]
[‘a’..’g’] “abcdefg”
23
Let and Where
• Writing readable code is very important
• Let and where constructs are helpful for improving code readability
24
Let Expressions
• The “let … in …” construct is an expression
• It can be used anywhere expressions are allowed
ghci> let x = 9 in x+1
10
ghci> 4 * (let x = 9 in x+1) + 2
42
25
Let Expressions
cylinder :: Double -> Double -> Double
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r * r
in sideArea + 2 * topArea
• Note that all sub-expressions (sideArea, topArea) are aligned to the same
column; otherwise, it causes a parse error
26
Where Clauses
• The where clause is bound to a surrounding syntactic construct, like the
pattern matching line of a function definition
cylinder' :: Double -> Double -> Double
cylinder' r h = sideArea + 2 * topArea
where sideArea = 2 * pi * r * h
topArea = pi * r * r
• Note that all sub-expressions (sideArea, topArea) are aligned to the same
column; otherwise, it causes a parse error
27
Let vs. Where
• The let expressions allow us to bind variables everywhere
• The where clauses can only bind variables at the end of a function
ghci> 1 + (let x = 10 in 2 * x)
21
ghci> 1 + (2 * x where x = 10)
<interactive>:2:12: error: parse error on input ‘where’
28
Let vs. Where
• Variables in let are local; they do not span across guards
29
Recursion
• Haskell does not have loops in the traditional sense
• Example: factorial
30
Recursion
• The definition of a recursive function has two cases
• Recursive case: find a subproblem, call the same function to solve the
subproblem, use the sub-result to create the result for the bigger problem
31
Example: maximum
• Write a function that returns the maximum element of a list
• Code (using the max function for two elements from Prelude)
myMaximum :: Ord a => [a] -> a
myMaximum [] = error "Empty List"
myMaximum [x] = x
myMaximum (x:xs) = max x (myMaximum xs)
32
Example: Quick Sort
• Quick sort only needs four lines of code!
• Code
quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort (x:xs) = smaller ++ [x] ++ larger
where smaller = quicksort [a | a<-xs, a<=x]
larger = quicksort [a | a<-xs, a>x]
33
Example: replicate
• replicate (from Prelude) takes an Int and a value, and returns a list that
has the specified number of repetitions of the value
• Code
myReplicate :: Int -> a -> [a]
myReplicate n v
| n <= 0 = []
| otherwise = v : myReplicate (n - 1) v
34
Example: take
• take (from Prelude) takes an Int n and a list, and returns the first n
elements of the list
• Code
myTake :: Int -> [a] -> [a]
myTake n _
| n <= 0 = []
myTake _ [] = []
myTake n (x:xs) = x : myTake (n-1) xs
35
Example: zip
• zip (from Prelude) takes two lists and zips them together. It truncates the
longer list to match the length of the shorter one.
• For example, zip [1, 2, 3] [7, 8] returns [(1, 7), (2, 8)]
36
Recursion with Auxiliary Functions
• A function may need additional parameters to track some information but
those additional parameters should be invisible to the client
37
Example: findFirst
• findFirst takes a value V and a list as input, and returns the position of the
first occurrence of V in the list (0-indexed). If V does not exist in the list, it
returns -1
• The findFirst function should only have two parameters, but we need an
additional parameter to track the index
findFirst :: Eq a => a -> [a] -> Int
findFirst v xs = findFirstAux 0 v xs
39
Mutual Recursion
• Functions can be defined using mutual recursion, where two or more
functions are defined recursively in terms of each other
40
Mutual Recursion
• Another example: two functions (evens and odds) that select the elements
from a list at all even and odd positions (0-indexed)
41
Efficiency of Recursion
• One may complain that a recursive function is less efficient than the
iterative version because the former needs to maintain a large call stack
42
Efficiency of Recursion
• How is sumTo 3 calculated?
sumTo 3
= 3 + sumTo 2
= 3 + (2 + sumTo 1)
= 3 + (2 + (1 + sumTo 0)))
= 3 + (2 + (1 + 0)))
= 3 + (2 + 1)
= 3 + 3
= 6
*conceptually, without thunk (unevaluated expressions)
• The calculation starts from the base case, but the function needs to
decrease the parameter to reach the base case
43
Efficiency of Recursion
sumTo 3
= 3 + sumTo 2
= 3 + (2 + sumTo 1)
= 3 + (2 + (1 + sumTo 0)))
= 3 + (2 + (1 + 0)))
= 3 + (2 + 1)
= 3 + 3
= 6
• Many frames are saved on the stack, because the return value of a
recursive call is used by the caller to build the result
• What if we don’t need to use the return value to “build” the result?
44
Tail Recursion
• A function is tail recursive when a recursive call is the last thing executed
by the function
45
Tail Recursion
• How is sumToTailRec 3 calculated?
sumToTailRec 3
= sumToTailRecAux 3 0
= sumToTailRecAux 2 3
= sumToTailRecAux 1 5
= sumToTailRecAux 0 6
= 6
*conceptually, without thunk
• The return value of each recursive call is the same as the return value of
the next recursive call
• Don’t need to save all frames on the stack; reuse the current frame for
next recursive call!
46
Running Time
• Running time of sumTo/sumToTailRec 100,000,000
• Lazy evaluation and tail recursion optimization affect the running time
47
Tail Recursion: Example
• Compute the factorial
fact :: Integer -> Integer
fact 0 = 1
fact n = n * fact (n - 1)
48
Tail Recursion: Example
• Compute the length of a list
len :: [a] -> Int
len [] = 0
len (_:xs) = 1 + len xs
49