Lecture 3

Typeclasses

Common properties

maxInt :: Int -> Int -> Int
maxInt x y = if x > y then x else y
maxChar :: Char -> Char -> Char
maxChar x y = if x > y then x else y

Why not just? πŸ€”

max :: a -> a -> a
max x y = if x > y then x else y

πŸ‘©β€πŸ”¬ The above type signature of max implies that the function can work with any type. But we don't know anything about the type!

Compare:

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

Polymorphisms

πŸ“œ Parametric polymorphism β€” same behavior for different types.

πŸ“œ Ad-hoc polymorphism β€” different behavior for different types.

Examples

  • The first element of a pair
  • Reversing a list
  • List length
  • Taking 5 elements from a list
  • Function composition
  • Number addition
  • Equality
  • Comparison
  • Conversion to string
  • Parsing from a string

πŸš‚ Parametric

🀸 Ad-hoc

class

  β”Œβ”€β”€ "class" keyword
  β”‚
  β”‚      β”Œβ”€β”€ Typeclass name
  β”‚      β”‚
  β”‚      β”‚    β”Œβ”€β”€ Type variable
  β”‚      β”‚    β”‚
  β”‚      β”‚    β”‚   β”Œβ”€β”€ "where" keyword     
class Display a where

    display :: a -> String
      β”‚          β”‚
      β”‚          └─ method type signature
      β”‚
  method name

instance

class Display a where
    display :: a -> String
instance Display Bool where
    display False = "false"
    display True  = "true"
greet :: Display a => a -> String
greet val = "Hello, " ++ display val
displayBoth :: (Display a, Display b) => a -> b -> String
displayBoth a b = display a ++ " and " ++ display b
instance Display Char where
    display c = [c]
ghci> :t greet
greet :: Display a => a -> String
ghci> :t display
display :: Display a => a -> String

ghci> greet 'A'
"Hello, A"
ghci> displayBoth 'x' True
"x and true"

Separation of concerns

data

class

instance

What is stored inside?

What we can do with this?

How we implement this behavior for that data?

Default methods

class Display a where
    {-# MINIMAL display #-}

    display :: a -> String
    
    displayList :: [a] -> String
    displayList l =
        "[" ++ intercalate ", " (map display l) ++ "]"

🐎 More performant methods

🦎 Different behaviour

🐘 Big typeclasses

🐰 Small typeclasses

🐁 Smaller possibility of error

🦁 Easier to write instances

Not Java interfaces

displayList :: Display a => [a] -> String

πŸ¦Έβ€β™€οΈ Typeclasses take power, not grant.

ghci> displayList [True, False, True]
"[true, false, true]"

ghci> displayList "Hello!"
"[H, e, l, l, o, !]"
ghci> displayList [True, 'X']

<interactive>:31:20: error:
    β€’ Couldn't match expected type β€˜Bool’ with actual type β€˜Char’
    β€’ In the expression: 'X'

How to read Haskell code?

🚫 displayList takes a list of values that can be converted to String.

πŸ‘ displayList takes a list of values of the same type and this type can be converted to String.

{-# LANGUAGE #-}

{-# LANGUAGE InstanceSigs #-}

module Display where

class Display a where
    display :: a -> String
    
instance Display Char where
    display :: Char -> String  -- πŸ‘ˆ needed for this
    display c = [c]

ℹ️ GHC has features not enabled by default. Use {-# LANGUAGE #-} pragma at the top of your file to enable them.

Standard Typeclasses

Eq β€” check for equality

Ord β€” compare

Show β€” convert to String

Read β€” parse from String

Bounded β€” has minimal and maximal value

Enum β€” is an enumeration

Num β€” a number (addition, multiplication, subtraction, etc.)

ghci> :info Bounded
type Bounded :: * -> Constraint
class Bounded a where
  minBound :: a
  maxBound :: a
  {-# MINIMAL minBound, maxBound #-}
  	-- Defined in β€˜GHC.Enum’
instance Bounded Word -- Defined in β€˜GHC.Enum’
instance Bounded Int -- Defined in β€˜GHC.Enum’
...

Eq

class Eq a where
    {-# MINIMAL (==) | (/=) #-}
    
    (==), (/=) :: a -> a -> Bool
    
    x /= y = not (x == y)
    x == y = not (x /= y)
ghci> :t (==)
(==) :: Eq a => a -> a -> Bool

ghci> 'x' == 'F'
False

ghci> [1..5] /= reverse [5, 4, 3, 2, 1]
False

ghci> "" == []
True

Equality

Ord

class (Eq a) => Ord a where
    {-# MINIMAL compare | (<=) #-}

    compare              :: a -> a -> Ordering
    (<), (<=), (>), (>=) :: a -> a -> Bool
    max, min             :: a -> a -> a

    compare x y = if x == y then EQ
                  else if x <= y then LT
                  else GT

    ...
data Ordering
    = LT  -- ^ Less
    | EQ  -- ^ Equal
    | GT  -- ^ Greater
sort   :: Ord a => [a] -> [a]
sortBy :: (a -> a -> Ordering) -> [a] -> [a]

Num

class Num a where
    (+), (-), (*)       :: a -> a -> a
    negate              :: a -> a
    abs                 :: a -> a
    signum              :: a -> a

    fromInteger         :: Integer -> a

    x - y               = x + negate y
    negate x            = 0 - x
ghci> :t 42
42 :: Num p => p
ghci> 42
42

🍬 Syntax sugar everywhere

ghci> fromInteger (42 :: Integer)
42

Type inference

ghci> check x y = x + y < x * y
ghci> :t check
check :: (Ord a, Num a) => a -> a -> Bool

deriving

data Color
    = Red
    | Green
    | Blue
    deriving (Eq, Ord, Show, Read, Enum, Bounded, Ix)

βš—οΈ GHC can automatically generate instances for you*

data Bit
    = Zero
    | One
    deriving (Num)
<interactive>:5:15: error:
    β€’ Can't make a derived instance of β€˜Num Bit’:
        β€˜Num’ is not a stock derivable class (Eq, Show, etc.)
        Try enabling DeriveAnyClass
    β€’ In the data declaration for β€˜Bit’

Generaliz(s)edNewtypeDeriving

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

newtype Size = Size
    { unSize :: Int
    } deriving ( Show
               , Read
               , Eq
               , Ord
               , Enum
               , Bounded
               , Ix
               , Num
               , Integral
               , Real
               , Bits
               , FiniteBits
               )

βš—οΈ For newtype, you can derive any typeclass of the original type

Algebra

class Semigroup a where
    (<>) :: a -> a -> a

βš—οΈ Typeclass for smashing things together

Equivalent typeclass

class Appendable a where
    append :: a -> a -> a
instance Semigroup [a] where
    (<>) = (++)

ℹ️ Standard instances

πŸ‘©β€πŸ”¬ But with laws! Associativity:

a <> (b <> c) \equiv (a <> b) <> c
instance Semigroup Bool where
    (<>) = ??? -- && or || , which one to choose ????

newtype again

newtype Any = Any { getAny :: Bool }
newtype All = All { getAll :: Bool }

βš—οΈ newtypes help implement different behaviour for the same type

instance Semigroup Any where
    Any x <> Any y = Any (x || y)
instance Semigroup All where
    All x <> All y = All (x && y)
ghci> Any False <> Any True
Any {getAny = True}
ghci> All False <> All True
All {getAll = False}

ghci> Any False <> All True

<interactive>:4:14: error:
    β€’ Couldn't match expected type β€˜Any’ with actual type β€˜All’
    β€’ In the second argument of β€˜(<>)’, namely β€˜All True’

Everything is Semigroup!

newtype Any =
    Any { getAny :: Bool }
newtype All =
    All { getAll :: Bool }
newtype Sum a =
    Sum { getSum :: a }
newtype Product a =
    Product { getProduct :: a }
newtype First a =
    First { getFirst :: a }
newtype Last a =
    Last { getLast :: a }
Ordering
Maybe a
[a]
(a, b)
...

Booleans with ||

Booleans with &&

Numbers with +

Numbers with *

Anything with taking first

Anything with taking last

Okay, not really everything...

newtype Sub a = Sub { getSub :: a }

instance Num a => Semigroup (Sub a) where
    Sub x <> Sub y = Sub (x - y)

πŸ”’ Numbers with subtraction (-)

1 - (2 - 3) \neq (1 - 2) - 3

🚫 Associativity doesn't hold!

Algebra, part 2

class Semigroup a => Monoid a where
    mempty :: a

βš—οΈ Smashing with a neutral element

instance Monoid [a] where
    mempty = []

Standard instances

πŸ‘©β€πŸ”¬ Laws again!

x <> \mathrm{mempty} \equiv x
instance Monoid Any where
    mempty = Any False

Right identity

\mathrm{mempty} <> x \equiv x

Left identity

instance Monoid All where
    mempty = All True
instance Num a => Monoid (Sum a) where
    mempty = Sum 0
instance Num a => Monoid (Product a) where
    mempty = Product 1

Not everything is Monoid!

newtype First a = First { getFirst :: a }

βš—οΈ Not every data type has a neutral element for <>

instance Semigroup (First a) where
    a <> _ = a
instance Monoid (First a) where
    mempty = ???

We need: mempty <> x ≑ x

Modules

base has two First data types

newtype First a =
    First { getFirst :: a }
newtype First a =
    First { getFirst :: Maybe a }

Data.Semigroup

Data.Monoid

Kind

Be kind to us, Haskell πŸ™

πŸ“œ Kind β€” a type of a type.

ghci> :k Int
Int :: *

ghci> :k String
String :: *
ghci> :k Maybe
Maybe :: * -> *

ghci> :k []
[] :: * -> *

ghci> :k Either
Either :: * -> * -> *

ghci> :k (->)
(->) :: * -> * -> *

πŸ“œ Types like Maybe, Either, etc. are called type constructors.

Kindly check your types

πŸ‘©β€πŸ”¬ The following type signature doesn't compile because the function arrow expects two types of kind * but Maybe is * -> *

maybeToList :: Maybe -> [a]
interactive>:8:16: error:
    β€’ Expecting one more argument to β€˜Maybe’
      Expected a type, but β€˜Maybe’ has kind β€˜* -> *’

πŸ‘©β€πŸ”¬ In some sense, type constructors are like functions: they take arguments (other types) to become complete types. They can also be partially applied!

ghci> :k Either
Either :: * -> * -> *

ghci> :k Either Int
Either Int :: * -> *

ghci> :k Either Int String
Either Int String :: *

Why?

πŸ‘©β€πŸ”¬ Haskell allows polymorphism over type constructors

Typeclass for type constructors

ℹ️ A typeclass for creating singleton "containers" from values

class Singleton f where
    singleton :: a -> f a

From the above typeclass definition we deduce the following facts:

  1. It's for type constructors (Maybe, [], etc.) and not e.g. Int
  2. Value types inside the type constructor should be the same.
instance Singleton Maybe where
    singleton :: a -> Maybe a
    singleton x = Just x
instance Singleton [] where
    singleton :: a -> [a]
    singleton x = [x]
ghci> singleton 3 :: Maybe Int
Just 3

ghci> singleton 3 :: [Int]
[3]
ghci> :t singleton 
singleton :: Singleton f => a -> f a

Functor

ℹ️ Mapping values inside context f

class Functor f where
    fmap :: (a -> b) -> f a -> f b
instance Functor Maybe      where ...
instance Functor []         where ...
instance Functor (Either e) where ...

ℹ️ fmap is a generalization of map

ghci> :t map
map :: (a -> b) -> [a] -> [b]

ghci> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
ghci> fmap (+ 5) (Just 7)
Just 12
ghci> fmap not [True, False, True]
[False,True,False]
ghci> fmap (drop 8) (Right [0 .. 10])
Right [8,9,10]
ghci> fmap (drop 8) (Left "Hello, Haskell!")
Left "Hello, Haskell!"

Functor laws

ℹ️ Identity function

πŸ’Ž Correct Maybe instance

id :: a -> a
id x = x
instance Functor Maybe where
    fmap :: (a -> b) -> Maybe a -> Maybe b
    fmap _ Nothing  = Nothing
    fmap f (Just x) = Just (f x)

Functor law 1: IdentityΒ 

Functor law 2: Composition

\mathrm{fmap} \ \mathrm{id} \equiv \mathrm{id}
\mathrm{fmap} \ (f\ .\ g) \equiv \mathrm{fmap} \ f \ . \ \mathrm{fmap} \ g

🐞 Incorrect Maybe instance

instance Functor Maybe where
    fmap :: (a -> b) -> Maybe a -> Maybe b
    fmap f m = Nothing

Folds

Folds for lists

         "step" function
               β”‚
          β”Œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”
foldr :: (a -> b -> b) -> b -> [a] -> b
                          β”‚     β”‚     β”‚
                          β”‚     β”‚     └─ final result
        initial value  β”€β”€β”€β”˜     β”‚
                                β”‚
                         list of values
ghci> foldr (+) 0 [1 .. 5]
15

ghci> foldr (*) 3 [2, 4]
24

Everything is a Fold!

Folds can be used to express almost everything you need!

  • Sum of elements

  • Length

  • Pair of length and sum (to get average)

  • The first element (aka head)

  • Index of the first zero

  • List of all elements

  • List of every second element

  • ...

πŸ‘©β€πŸ”¬ However, this doesn't mean that you always must use a fold. Sometimes, an explicit recursive function is easier to read.

How foldr folds

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr _ z [] = z
foldr f z (x : xs) = f x (foldr f z xs)
foldr f z [1,2,3] == f 1 (f 2 (f 3 z)) 
foldr (+) 0 [1, 2, 3]
1  :  2  :  3  : []
1  + (2  + (3  + 0))

πŸ‘©β€πŸ”¬ foldr f z replaces every list constructor (:) with f and [] with z

πŸ‘©β€πŸ”¬ Use foldr when the function is lazy on the second argument

ghci> foldr (&&) True (repeat False)
False

ghci> foldr (\x _ -> Just x) Nothing [1 .. ]
Just 1

How foldl/foldl' folds

foldl :: (b -> a -> b) -> b -> [a] -> b
foldl _ z [] = z
foldl f z (x : xs) = foldl f (f z x) xs
foldl f z [1,2,3] == f (f (f z 1) 2) 3
foldl (+) 0 [1, 2, 3]
        1  :  2   :  3  : []
((0  +  1) +  2)  +  3

⚠️ foldl always leaks memory due to its nature

πŸ‘©β€πŸ”¬ foldl' is a strict version of foldl

ghci> foldl' (\acc x -> x + acc) 0 [1 .. 10]
55

ghci> foldl' (\acc _ -> acc + 1) 0 [1 .. 10]
10

foldr vs foldl

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

πŸ‘©β€πŸ”¬Β Haskell has many folds. Choose between foldrΒ / foldl'

ghci> foldr (-) 0 [1..5]
3
0 - (1 - (2 - (3 - (4 - 5))))
ghci> foldl' (-) 0 [1..5]
-15
((((0 - 1) - 2) - 3) - 4) - 5

Foldable

ℹ️ Folding any structure t that contains elements

class Foldable t where
    foldr :: (a -> b -> b) -> b -> t a -> b
    foldMap :: Monoid m => (a -> m) -> t a -> m
    
    ... and 15 more methods ...
ghci> :t foldr
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

ghci> :t sum
sum :: (Foldable t, Num a) => t a -> a

ghci> :t concat
concat :: Foldable t => t [a] -> [a]

πŸ™Œ Now the standard Haskell library makes sense!

ghci> foldr (-) 10 (Just 3)
-7

Strict evaluation

πŸ¦₯ Laziness can lead to space leaks

sum :: [Int] -> Int
sum = go 0
  where
    go :: Int -> [Int] -> Int
    go acc []       = acc
    go acc (x : xs) = go (acc + x) xs
sum [3, 1, 2]
  = go 0 [3, 1, 2]
  = go (0 + 3) [1, 2]
  = go ((0 + 3) + 1) [2]
  = go (((0 + 3) + 1) + 2) []
  = ((0 + 3) + 1) + 2
  = (3 + 1) + 2
  = 4 + 2
  = 6

πŸžπŸ” Debugging in Haskell: Equational Reasoning

{-# LANGUAGE BangPatterns #-}

πŸ’₯ You can force evaluation of lazy computations* with bangs !

{-# LANGUAGE BangPatterns #-}

sum :: [Int] -> Int
sum = go 0
  where
    go :: Int -> [Int] -> Int
    go !acc []       = acc
    go !acc (x : xs) = go (acc + x) xs
sum [3, 1, 2]
  = go 0 [3, 1, 2]
  = go 3 [1, 2]
  = go 4 [2]
  = go 6 []
  = 6

🚰 No more space leaks!

* to some degree

Use strict/lazy evaluation wisely

More sources

Lecture 3: Typeclasses

By Haskell Beginners 2022

Lecture 3: Typeclasses

Typeclasses, instances, deriving

  • 2,825