Lecture 2

Data

Pattern Matching

Patterns

πŸ“œ Pattern β€” specific value or shape of input.

case-of

f :: type
f ... = case var of
    pat1 -> result1
    pat2 -> result2
    ...
    patN -> resultN

Syntax

f :: type
f pat1 = result1
f pat2 = result2
...
f patN = resultN

Examples

not :: Bool -> Bool
not True  = False
not False = True
isZero :: Int -> Bool
isZero 0 = True
isZero n = False
eval :: Char -> Int -> Int -> Int
eval op x y = case op of
    '+' -> x + y
    '-' -> x - y
    '*' -> x * y
    '/' -> div x y
    _ -> 0
    ^ watch for indentation!

Patterns on lists

isEmpty :: [Int] -> Bool
isEmpty [] = True
isEmpty _  = False
sumOfTwoInThree :: [Int] -> Int
sumOfTwoInThree [x, _, y] = x + y
sumOfTwoInThree _         = 0
oneOrTwoZeroes :: [Int] -> Bool
oneOrTwoZeroes l = case l of
    [0]    -> True
    [0, 0] -> True
    _      -> False
-- check if a list has at least two elements
atLeastTwo :: [Int] -> Bool
atLeastTwo [_, _] = True
atLeastTwo _      = False

Does the following work?

❌ No! Because it checks only on lists of size two.

Structural List Patterns

[]      
x : xs

A list can be only one of those two:

1️⃣ Empty list

2️⃣ An element prepended to a list

      β”Œβ”€β”€ connecting head and tail
      β”‚   `:` is both operator and pattern
      β”‚
    x : xs
    β”‚   β”‚
    β”‚   └── tail
  head

Wait! What about list literals [x, y, z]?

Syntax sugar 🍬

[ 3 ,  1 , 2 ]

  3 :  1 :  2 : []

  3 : (1 : (2 : []))

πŸͺ™ Heads or Tails?

headOrDef :: Int -> [Int] -> Int
headOrDef def []      = def
headOrDef _   (x : _) = x
dropHead :: [Int] -> [Int]
dropHead []       = []
dropHead (_ : xs) = xs
secondIsZero :: [Int] -> Bool
secondIsZero (_ : 0 : _) = True
secondIsZero _ = False

πŸ”„ List Recursion

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

⚠️ Warning!

🐌 Slow implementation!

count :: Int -> [Int] -> Int
count n list = go 0 list
  where
    go :: Int -> [Int] -> Int
    go acc list =
        if null list
        then result
        else if head list == n
        then go (acc + 1) (tail list)
        else go  acc      (tail list)
count :: Int -> [Int] -> Int
count n list = go 0 list
  where
    go :: Int -> [Int] -> Int
    go acc [] = acc
    go acc (x : xs)
        | x == n    = go (acc + 1) xs
        | otherwise = go  acc      xs

Improving previous examples

πŸ‘©β€πŸ”¬ Always use pattern matching on lists instead of unsafe

head and tail functions whenever you need either of them.

What's wrong, Haskell?

isEmpty :: [Int] -> True
isEmpty _  = False
isEmpty [] = True
same :: Int -> Int -> True
same x x = True
same _ _ = False

πŸ’» Code

🚫 Error

headOrDef :: Int -> [Int] -> Int
headOrDef def l       = def
headOrDef _   (x : _) = x

πŸ‘©β€πŸ”¬ Incorrect order of patterns

πŸ‘©β€πŸ”¬ Variable is a "catch-all" pattern

πŸ‘©β€πŸ”¬ Patterns on variable names are not supported

head :: [Int] -> Int
head (x : _) = x

πŸ‘©β€πŸ”¬ Patterns don't cover all possible cases

isGreeting :: String -> Bool
isGreeting str = case str of
    'H' : 'i' : '!' : _ -> True
    _ -> False

πŸ‘©β€πŸ”¬ No errors here,

just looks ugly.

Totality

example :: Bool -> [Int] -> Int
example True  []     = 0
example False [x, y] = x + y

πŸ“œ A function is total if it is defined for all inputs of its corresponding type, or in other words, if a function returns the output on any possible values of the input types.

βš—οΈ GHC checks if a function is partial due to non-exhaustive patterns.

πŸ“œ Partial β€” non-total.

<interactive>:32:1: warning: [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In an equation for β€˜example’:
        Patterns not matched:
            True (_:_)
            False (_:_:_:_)
            False [_]
            False []

Data

Tuples

ghci> :t ('x', True)
('x', True) :: (Char, Bool)

ghci> :t ([True, False], "abc", 'F')
([True, False], "abc", 'F') :: ([Bool], [Char], Char)
ghci> fst ('x', True)
'x'

ghci> snd ('x', True)
True

ghci> fst (3, True, False)

<interactive>:39:5: error:
    β€’ Couldn't match expected type β€˜(a, b0)’
                  with actual type β€˜(a0, Bool, Bool)’

Working with tuples

splitAtPos3 :: [Int] -> ([Int], [Int])
splitAtPos3 l = (take 3 l, drop 3 l)
ghci> splitAtPos3 [1..10]
([1,2,3],[4,5,6,7,8,9,10])
ghci> splitAtPos3 [3, 1]
([3,1],[])

Returning

Matching

showTriple :: (Bool, Int, String) -> String
showTriple (b, n, string) =
    if b
    then "The number is: " ++ show n
    else "The string is: " ++ string
ghci> showTriple (True, 42, "hello")
"The number is: 42"
ghci> showTriple (False, 42, "hello")
"The string is: hello"

Algebraic Data Type (ADT)

ADT β€” a type formed by combing other types using either

product or sum types.

Product type

Sum type

Combining types using both

Choice of types, either one or another

Product Type

Product type β€” zero or more types combined

String

Int

Bool

"Hello!"
42
True
("Hello!", 42, True)
("Byeeee", 15, False)
("", 0, True)

All possible combinations including values of all types

data: Product

      β”Œβ”€β”€ data type name
      β”‚
      β”‚       β”Œβ”€β”€ Constructor name
      β”‚       β”‚
data User = MkUser String Int Bool
                     β”‚     β”‚    β”‚
                     β””β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”˜
                           β”‚
                           β”‚
                      Field types

data: Working with data

data User = MkUser String Int Bool
    deriving (Show)  -- to display our type in GHCi

Getters

getUserName :: User -> String
getUserName (MkUser name _ _) = name

Setters

setUserName :: String -> User -> User
setUserName name (MkUser _ age isTired) = MkUser name age isTired
ghci> :t MkUser
MkUser :: String -> Int -> Bool -> User

ghci> getUserName (MkUser "John" 29 True)
"John"

ghci> setUserName "Ivan" (MkUser "John" 29 True)
MkUser "Ivan" 29 True
getUserAge :: User -> Int
getUserAge (MkUser _ age _) = age

data: Records

ghci> john = MkUser {userAge = 29, userIsTired = True, userName = "John"}
ghci> john
MkUser {userName = "John", userAge = 29, userIsTired = True}

ghci> userName john
"John"

ghci> ivan = john { userName = "Ivan", userIsTired = False }
ghci> ivan
MkUser {userName = "Ivan", userAge = 29, userIsTired = False}
data User = MkUser
    { userName    :: String
    , userAge     :: Int 
    , userIsTired :: Bool
    } 
userName    :: User -> String
userAge     :: User -> Int
userIsTired :: User -> Bool

Generates the following top-level functions

βž• Initialization using names

βž• Record-update syntax

Sum Type

Sum type β€” choice of zero or more types

String

Int

Bool

"Hello!"
42
True
"Hello!"
42
True

Any possible option of a value from each type

Sum Types: Enumerations

data Color
    = Red
    | Green
    | Blue
data Bool
    = False
    | True
showColor :: Color -> String
showColor color = case color of
    Red   -> "red"
    Green -> "green"
    Blue  -> "blue"
ghci> showColor Blue
"blue"

ghci> map showColor [Red, Green, Blue, Green]
["red","green","blue","green"]

Sum Types

data Result
    = Error String
    | Ok Int
divide :: Int -> Int -> Result
divide _ 0 = Error "Division by zero!"
divide x y = Ok (div x y)
ghci> showResult (divide 15 0)
"Error: Division by zero!"

ghci> showResult (divide 15 3)
"Ok: 5"
showResult :: Result -> String
showResult (Error msg) = "Error: " ++ msg
showResult (Ok result) = "Ok: " ++ show result

A data type with 2 constructors

Each constructor has one field

data Answer
    = NoResult
    | Result Int
data Property
    = Padding Int
    | Clickable Bool Int
    | Description String

Recursive data types

data IntList
    = Empty
    | Cons Int IntList
length :: IntList -> Int
length Empty = 0
length (Cons _ xs) = 1 + length xs

1️⃣ Empty list

2️⃣ An element prepended to a list

nzeroes :: Int -> IntList
nzeroes 0 = Empty
nzeroes n = Cons 0 (nzeroes (n - 1))
ghci> nzeroes 3
Cons 0 (Cons 0 (Cons 0 Empty))

ghci> length (nzeroes 126)
126

type

type MyTriples = [(Int, Bool, String)]
type IntPredicate = Int -> Bool

ℹ️ Use the type keyword to giveΒ  another name to an existing type

type String = [Char]
type FilePath = String

Standard type aliases

⚠️ The type is the same! It's just a different name.

type Attack  = Int
type Defense = Int
type Health  = Int
damage :: Attack -> Defense -> Health -> Health
damage atk def hp = hp + def - atk

newtype

newtype β€” lightweight wrapper for an existing type. Can only have:

  • Exactly one constructor
  • Exactly one field
type Attack  = Int
type Defense = Int
type Health  = Int
newtype Attack  = MkAttack Int
newtype Defense = MkDefense Int
newtype Health  = MkHealth Int
damage :: Attack -> Defense -> Health -> Health
damage (MkAttack atk) (MkDefense def) (MkHealth hp) =
    MkHealth (hp + def - atk)

πŸ”° Benefits

  1. No runtime cost
  2. Literally new type

πŸ’° Costs

  1. Wrapping and unwrapping

Polymorphism

Parametric polymorphism

dup :: a -> (a, a)
dup x = (x, x)

⬆️ Specific types start with an upper letter: String, Int, Bool, ...

⬇️ Type variables start with a lower letter: a, b, c, f, m, ...

ghci> dup 'x'
('x','x')

ghci> :t dup 'x'
dup 'x' :: (Char, Char)

ghci> dup True
(True,True)

ghci> :t dup True
dup True :: (Bool, Bool)

Real types

fst :: (a, b) -> a
snd :: (a, b) -> b

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

head    :: [a] ->  a
tail    :: [a] -> [a]
reverse :: [a] -> [a]

take :: Int -> [a] -> [a]
drop :: Int -> [a] -> [a]

zip :: [a] -> [b] -> [(a, b)]
map    :: (a -> b)    -> [a] -> [b]
filter :: (a -> Bool) -> [a] -> [a]

Hoogle

Hoogle β€” Haskell search engine to search Haskell package database by name, module or even type.

Polymorphic types

πŸ‰ A treasure chest always contains gold and some reward

data Chest a = MkChest
    { chestGold     :: Int
    , chestTreasure :: a
    }
type BronzeChest = Chest Armor
type SilverChest = Chest (Sword, Armor)
type GoldenChest = Chest (Artifact, Sword, [Gemstone])
ghci> :t MkChest
MkChest :: Int -> a -> Chest a

ghci> MkChest 100 True
MkChest { chestGold = 100
        , chestTreasure = True}
data RewardChest
    = Bronze BronzeChest
    | Silver SilverChest
    | Golden GoldenChest
reward :: Dragon -> RewardChest
reward ... = ...

Common types

data Maybe a
    = Nothing
    | Just a
data Either a b
    = Left a
    | Right b

ℹ️ Optional value

ℹ️ Choice between two types

ℹ️ List

data [] a
    = []
    | a : [a]
data List a
    = Empty
    | Cons a (List a)

Simpler version (not in base)

Common functions

find :: (a -> Bool) -> [a] -> Maybe a

ghci> find (> 4) [3, 1, 2]
Nothing
ghci> find (> 4) [3, 5, 1, 2, 10]
Just 5

ℹ️ Find an optional value

fromMaybeInt :: Maybe Int -> Int
fromMaybeInt Nothing  = 0
fromMaybeInt (Just n) = n

ℹ️ Optional number to zero default

ℹ️ Lists of different types

partitionEithers :: [Either a b] -> ([a], [b])
ghci> partitionEithers [Left 3, Right "hi!", Left 5]
([3,5],["hi!"])

Back to Functions

Eta-reduction, part 1

\forall x : f(x) \equiv g(x)\ \ \mathrm{means}\ \ f \equiv g
showInt :: Int -> String
showInt n = show n
showInt :: Int -> String
showInt = show

Ξ·-reduction, part 2

onlyEven :: [Int] -> [Int]
onlyEven xs = filter even xs
onlyEven :: [Int] -> [Int]
onlyEven = filter even
prod :: [Int] -> [Int] -> [Int]
prod xs ys = zipWith (*) xs ys
prod :: [Int] -> [Int] -> [Int]
prod = zipWith (*)
ghci> :t zipWith
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]

ghci> zipWith (*) [3, 1, 2] [10, 20, 30]
[30,20,60]

πŸ‘©β€πŸ”¬ You don't always want to eta-reduce because variable names can be helpful in some situations

Function Composition

☎️ Function composition operator: the dot (.) operator

(.) :: (b -> c) -> (a -> b) -> a -> c
(.) f g = \x -> f (g x)

ELI5 Composition

\mathrm{defrost} :: 🧊 \rightarrow πŸ₯¦
\mathrm{cook} :: πŸ₯¦ \rightarrow πŸ₯˜
\mathrm{cookFrozen} :: 🧊 \rightarrow πŸ₯˜
\mathrm{cookFrozen} = \mathrm{cook}\ .\ \mathrm{defrost}

Function Composition

(.) :: (b -> c) -> (a -> b) -> a -> c
(.) f g = \x -> f (g x)

Examples

ghci> (length . show) True
4

ghci> map (length . words) ["Hi all", "bi dup boo", "x"]
[2,3,1]

ghci> map (even . length . words) ["Hi all", "bi dup boo", "x"]
[True,False,False]

Composition + Eta-reduction

πŸ›  A function that returns first 5 lists of even length

takeEven5 :: [[a]] -> [[a]]
takeEven5 list = take 5 (filter (\l -> even (length l)) list)

1️⃣ Rewrite lambda to composition

takeEven5 list = take 5 (filter (\l -> (even . length) l) list)

2️⃣ Eta-reduce lambda

takeEven5 list = take 5 (filter (even . length) list)

3️⃣ Rewrite main function to composition

takeEven5 list = (take 5 . filter (even . length)) list
takeEven5 = take 5 . filter (even . length)

4️⃣ Eta-reduce main function

More sources

Lecture 2: Data types

By Haskell Beginners 2022

Lecture 2: Data types

Custom types in Haskell

  • 1,121