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:
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 (-)
π« 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!
instance Monoid Any where
mempty = Any False
Right identity
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:
- It's for type constructors (Maybe, [], etc.) and not e.g. Int
- 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
π 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