0% found this document useful (0 votes)
3 views

2-FuncSyntax

The document covers various aspects of syntax in Haskell functions, including pattern matching, conditionals, list comprehensions, and recursion. It provides examples of how to implement these concepts, such as using pattern matching for logical operations and defining functions like factorial and quicksort. Additionally, it discusses the importance of readability in code through constructs like 'let' and 'where'.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views

2-FuncSyntax

The document covers various aspects of syntax in Haskell functions, including pattern matching, conditionals, list comprehensions, and recursion. It provides examples of how to implement these concepts, such as using pattern matching for logical operations and defining functions like factorial and quicksort. Additionally, it discusses the importance of readability in code through constructs like 'let' and 'where'.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 49

CMPT383 Comparative Programming Languages

Lecture 2: Syntax in Functions

Yuepeng Wang
Spring 2025

1
Overview
• Pattern matching

• Conditionals

• List comprehensions

• Let and where

• 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

• When defining functions in Haskell, we can create separate function


bodies for different patterns

• It leads to simple, readable code

• 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

• Second line: if the first argument is False, the result is False


• The _ matches anything but drops its value
4
Pattern Matching: Example
• The behavior of myAnd is the same as the logical and

ghci> myAnd True True


True
ghci> myAnd True False
False
ghci> myAnd False True
False
ghci> myAnd False False
False

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

• Example lucky :: Int -> String


lucky 7 = "Lucky"
lucky _ = "Out of luck"

• Execution results ghci> lucky 7


"Lucky"
ghci> lucky 1
"Out of luck"

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)

• With pattern matching

addPairs' :: (Int, Int) -> (Int, Int) -> (Int, Int)


addPairs' (x1, x2) (y1, y2) = (x1 + y1, x2 + y2)

7
Pattern Matching: Lists
• We can also pattern match against lists

• A common pattern to deconstruct a list is by its head and tail

• Example myHead :: [a] -> a


myHead [] = error "Empty List"
myHead (x:_) = x
error is a function from Prelude that throws an exception with a message

• 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 [] = …

func (x:xs) = … func xs …

• Example: compute the length of a list

myLength :: [a] -> Int


myLength [] = 0
myLength (_:xs) = 1 + myLength xs

9
Patterns of Lists
• [ ] matches an empty list

• [x] matches a singleton list (exactly one element)

• (x:xs) matches a list with at least one element

• The head is x, the tail is xs (could be empty)

• (x:y:xs) matches a list with at least two elements

• The head is x, second element is y, the rest of list is xs (could be empty)

• And many more, e.g., [x, y], (x:y:z:_), (_:_:_:_:_:_), etc


10
Patterns of Lists
• Suppose we have a pattern (x:y:xs)

List x y xs

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

[1,2] 1 2 []

"abc" 'a' 'b' "c"

[1] does not match

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

firstLetter :: String -> String


firstLetter "" = "Empty String"
firstLetter all@(x:xs) = "The fisrt letter of " ++ all ++ " is " ++ [x]

• It also works for tuples

firstElem :: (Int, Int) -> String


firstElem x@(x1, x2) = "The first elem of " ++ show x ++ " is " ++ show x1

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

• We should inspect the implementation and add the missing cases

• Sometimes it is convenient to use _ to match all values

badAddFixed :: [Int] -> Int


badAddFixed [] = 0
badAddFixed [x] = x
badAddFixed (x:y:_) = x + y

14
Conditionals: If-Then-Else
• Haskell has several different ways to write code involving conditions

• The If-Then-Else expression is a simple way

• 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

• Analogous to the mathematical function


1 if x >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"

• It is an expression; it can be used like any other Haskell expression

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}

• Haskell has a very similar notation to build a list

• A list comprehension is a way in Haskell to describe, filter, transform, and


combine lists

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

• Each generator value is used as an argument of the representative


expression
ghci> [ x^2 | x <- [1, 2, 3, 4, 5] ]
[1,4,9,16,25]
ghci> [ succ c | c <- "abcd" ]
"bcde"
succ is from Prelude; it means successor, the next one

20
List Comprehensions: Generators
• A list comprehension can have more than one generator, with successive
generators being separated by lemmas

• Every pair of values is produced

• 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

• A guard is a predicate (boolean expression)


• If the guard evaluates to True under a value, the value is retained
• If the guard evaluates to False, the value is discarded

ghci> [ x^2 | x <- [1, 2, 3, 4, 5] ]


[1,4,9,16,25]
ghci> [ x^2 | x <- [1, 2, 3, 4, 5], even x ]
[4,16]
even is from Prelude

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”

• It helps to write a list comprehension with a sequence of numbers being


the generator

squares n = [ x^2 | x <- [1..n] ]


foo = [ x*2 | x <- [1..10], x * 2 >= 12]
bar = [ x | x <- [50..100], x `mod` 7 == 3 ]

23
Let and Where
• Writing readable code is very important

• Let and where constructs are helpful for improving code readability

• We should use let or where if the code is getting complicated


• Break up long expressions
• Give the sub-expressions meaningful names

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

• It can be used to simplify longer definitions


cylinder :: Double -> Double -> Double
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r * r
in sideArea + 2 * topArea

25
Let Expressions
cylinder :: Double -> Double -> Double
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r * r
in sideArea + 2 * topArea

• In the example of cylinder surface area, the let expression allows us to


• Split one long expression into several short ones
• Give the sub-expression meaningful names

• 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

• Similar to let-in expressions, where clauses allow multiple sub-


expressions, separated by line break or semicolon

• 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

• Variables in where are visible to the entire function


baz :: Int -> String
baz x
| x > 0 = s
| otherwise = "not " ++ s
where s = "pos"

baz :: Int -> String


baz x
| x > 0 = let s = "pos" in s
| otherwise = "not " ++ s

29
Recursion
• Haskell does not have loops in the traditional sense

• It supports recursions that can be used instead of loops

• With recursions and conditionals, the language is Turing complete

• Example: factorial

factorial :: Integer -> Integer


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

30
Recursion
• The definition of a recursive function has two cases

• Base case: simple case where the result is straightforward

• 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

• Example: argument n decreases to n-1 or n/2. Base case is 0 or 1.

• Example: argument (x:xs) becomes xs. Base case is [ ] or [x].

31
Example: maximum
• Write a function that returns the maximum element of a list

• What is the type of the function?

• Ord a => [a] -> a

• 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!

• What is the type of the quick sort function?

• Ord a => [a] -> [a]

• 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

• What is the type of replicate?

• Int -> a -> [a]

• 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

• What is the type of take?

• Int -> [a] -> [a]

• 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)]

• What is the type of zip?

• [a] -> [b] -> [(a, b)]

• Code myZip :: [a] -> [b] -> [(a, b)]


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

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

• We can define multiple functions

• One or more auxiliary functions with additional parameters

• A “main” function for the client with a clean “interface”

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

findFirstAux :: Eq a => Int -> a -> [a] -> Int


findFirstAux _ _ [] = -1
findFirstAux i v (x:xs)
| v == x = i
| otherwise = findFirstAux (i + 1) v xs
38
Example: findFirst
• Problem: findFirstAux is also visible in the top-level scope

• We can hide auxiliary functions in the top-level scope using where

findFirst :: Eq a => a -> [a] -> Int


findFirst v xs = findFirstAux 0 v xs
where
findFirstAux :: Eq a => Int -> a -> [a] -> Int
findFirstAux _ _ [] = -1
findFirstAux i v (x:xs)
| v == x = i
| otherwise = findFirstAux (i + 1) 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

• Example: the even and odd functions (from Prelude)


myEven :: Int -> Bool
myEven 0 = True
myEven n = myOdd (n - 1)

myOdd :: Int -> Bool


myOdd 0 = False
myOdd n = myEven (n - 1)

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)

• What are the types of evens and odds?

• evens :: [a] -> [a] odds :: [a] -> [a]


evens :: [a] -> [a]
evens [] = []
evens (x:xs) = x : odds xs

odds :: [a] -> [a]


odds [] = []
odds (_:xs) = evens xs

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

• Example: sumTo function that computes the sum of integers from 0 to N

sumTo :: Integer -> Integer


sumTo 0 = 0
sumTo n = n + sumTo (n - 1)

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

• Example: sumToTailRec that computes the sum of integers from 0 to N


sumToTailRec :: Integer -> Integer
sumToTailRec n = sumToTailRecAux n 0

sumToTailRecAux :: Integer -> Integer -> Integer


sumToTailRecAux n r
| n == 0 = r
| otherwise = sumToTailRecAux (n - 1) (n + r)

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

Function Compiler Command Running time


sumTo ghc ~15s
sumToTailRec ghc ~18s
sumTo ghc -O2 ~5s
sumToTailRec ghc -O2 ~1s

• 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)

• Tail recursive version


factTailRec :: Integer -> Integer
factTailRec n = factTailRecAux n 1
where
factTailRecAux :: Integer -> Integer -> Integer
factTailRecAux 0 r = r
factTailRecAux n r = factTailRecAux (n - 1) (n * r)

48
Tail Recursion: Example
• Compute the length of a list
len :: [a] -> Int
len [] = 0
len (_:xs) = 1 + len xs

• Tail recursive version


lenTailRec :: [a] -> Int
lenTailRec xs = lenTailRecAux xs 0
where
lenTailRecAux :: [a] -> Int -> Int
lenTailRecAux [] l = l
lenTailRecAux (_:xs) l = lenTailRecAux xs (l + 1)

49

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy