522 lines
12 KiB
Org Mode
522 lines
12 KiB
Org Mode
#+TITLE: Type Classes
|
||
|
||
#+PROPERTY: header-args:haskell :results replace output
|
||
#+PROPERTY: header-args:haskell+ :noweb yes
|
||
#+PROPERTY: header-args:haskell+ :wrap EXAMPLE
|
||
|
||
* Summary
|
||
- Using Ad Hoc Polymorphism with Type classes
|
||
#+BEGIN_SRC haskell
|
||
-- How can you write a function to remove duplicates from a list of generic
|
||
-- elements? You need to rely on the user to provide a function to compare those
|
||
-- elements `a -> a -> Bool`
|
||
:{
|
||
unique :: (a -> a -> Bool) -> [a] -> [a]
|
||
unique _ [] = []
|
||
unique f (x:xs) = x : unique f (filter (not . f x) xs)
|
||
:}
|
||
|
||
unique (==) [1, 2, 1, 3, 3, 4, 5, 1]
|
||
|
||
-- Works but doesn't scale, more stuff you need to do on a generic `a` and more
|
||
-- functions you need to ask to the user
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
[1,2,3,4,5]
|
||
#+end_EXAMPLE
|
||
|
||
With type classes you can give a name to a group of related functions and then
|
||
you can provide an implementation of those functions for different types.
|
||
- Type class for ~Natural~
|
||
#+BEGIN_SRC haskell
|
||
:{
|
||
class Equality n where
|
||
equal :: n -> n -> Bool
|
||
|
||
instance Equality Int where
|
||
equal = (==)
|
||
|
||
unique :: Equality a => [a] -> [a]
|
||
unique [] = []
|
||
unique (x:xs) = x : unique (filter (not . equal x) xs)
|
||
:}
|
||
|
||
unique [1, 2, 1, 3, 3, 4, 5, 1] :: [Int]
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
[1,2,3,4,5]
|
||
#+end_EXAMPLE
|
||
- Composing Type Classes
|
||
#+BEGIN_SRC haskell
|
||
:{
|
||
class Equality n where
|
||
equal :: n -> n -> Bool
|
||
|
||
class ToString n where
|
||
toString :: n -> String
|
||
|
||
-- To implement Natural type class for a type you also need to implement
|
||
-- `Equality` and `ToString`
|
||
class (Equality n, ToString n) => Natural n where
|
||
add :: n -> n -> n
|
||
mul :: n -> n -> n
|
||
addIdentity :: n
|
||
mulIdentity :: n
|
||
:}
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
#+end_EXAMPLE
|
||
- Creating Default Implementations and Minimal Definitions
|
||
#+BEGIN_SRC haskell :eval never
|
||
data Ordering = LT | EQ | GT
|
||
|
||
instance Show Ordering where
|
||
show LT = "LT"
|
||
show EQ = "EQ"
|
||
show GT = "GT"
|
||
|
||
class Eq a => Ordering a where
|
||
compare :: a -> a -> Ordering
|
||
(<) :: a -> a -> Bool
|
||
(<=) :: a -> a -> Bool
|
||
(>) :: a -> a -> Bool
|
||
(>=) :: a -> a -> Bool
|
||
max :: a -> a -> a
|
||
min :: a -> a -> a
|
||
|
||
-- NOTE: we can implement all the functions in Ordering only based on `compare`
|
||
-- or `<=`, we can provide default implementations in the class definition and a
|
||
-- minimal set of function the user need to implement separated by `|`
|
||
|
||
class Eq a => Ordering a where
|
||
compare :: a -> a -> Ordering
|
||
-- Can be implemented based on `<=`
|
||
compare a b
|
||
| a == b = EQ
|
||
| a <= b = LT
|
||
| otherwise = GT
|
||
(<=) :: a -> a -> Bool
|
||
-- Can be implemented based on `compare`
|
||
(<=) a b = case compare a b of
|
||
GT => False
|
||
_ => True
|
||
-- ... same as the other functions, therefore either you implement `compare`
|
||
-- or `<=`
|
||
{-# MINIMAL compare | (<=) #-}
|
||
#+END_SRC
|
||
- Default Implementation only when an Instance of a Type Class is provided
|
||
#+BEGIN_SRC haskell
|
||
:set -XDefaultSignatures
|
||
|
||
:{
|
||
-- NOTE: no constraints on `a` in class definition header
|
||
class Redacted a where
|
||
redacted :: a -> String
|
||
-- only when `a` has an instance of `Show` then we can provide a default implementation
|
||
default redacted :: Show a => a -> String
|
||
redacted = show
|
||
|
||
data Username = Username String
|
||
data Password = Password String
|
||
|
||
instance Show Username where
|
||
show (Username s) = s
|
||
|
||
instance Redacted Username -- ok, implements Show -> we have a default implementation
|
||
|
||
-- error, `No instance for (Show Password)` -> no default implementation for `Redacted`
|
||
-- instance Redacted Password
|
||
|
||
instance Redacted Password where
|
||
redacted _ = "???"
|
||
:}
|
||
|
||
redacted $ Username "Gabriele"
|
||
redacted $ Password "nosecrets"
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
Gabriele
|
||
???
|
||
#+end_EXAMPLE
|
||
|
||
- Specifying Type Class Instances with Type Applications
|
||
TODO
|
||
|
||
- Wrapping Types with Newtype
|
||
TODO
|
||
|
||
- Understanding Higher Kinded Types and Polymorphism
|
||
TODO
|
||
|
||
- Deriving Instances (stock)
|
||
- Don't need language extensions
|
||
- Works only for (~Eq~, ~Ord~, ~Ix~, ~Show~, ~Read~, ~Enum~, ~Bounded~)
|
||
- Works only if the underlying types implement the type class
|
||
- It's not transitive, see ~CustomerWithID~, cannot implement ~Show~ when one
|
||
of the underglying types doesn't implement ~Show~ even if the type could
|
||
derive stock ~Show~.
|
||
#+BEGIN_SRC haskell
|
||
:{
|
||
data Customer = Customer
|
||
{ name :: String
|
||
, surname :: String
|
||
, email :: String
|
||
} deriving (Show, Eq, Ord)
|
||
|
||
newtype UserID = UserID String deriving Show
|
||
:}
|
||
|
||
UserID "7246daaf-bf40-4528-a9fe-923cb221cab3"
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
UserID "7246daaf-bf40-4528-a9fe-923cb221cab3"
|
||
#+end_EXAMPLE
|
||
|
||
#+BEGIN_SRC haskell
|
||
:{
|
||
newtype UserID = UserID String
|
||
|
||
data CustomerWithID = CustomerWithID
|
||
{ id :: UserID
|
||
, name :: String
|
||
, surname :: String
|
||
, email :: String
|
||
} deriving Show
|
||
:}
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
<interactive>:11:14: error:
|
||
• No instance for (Show UserID)
|
||
arising from the first field of ‘CustomerWithID’ (type ‘UserID’)
|
||
Possible fix:
|
||
use a standalone 'deriving instance' declaration,
|
||
so you can specify the instance context yourself
|
||
• When deriving the instance for (Show CustomerWithID)
|
||
#+end_EXAMPLE
|
||
|
||
- Deriving Instances (newtype)
|
||
- Can derive for newtypes non stock type classes that are implemented by the
|
||
underlying type
|
||
- Needs ~GeneralizedNewtypeDeriving~ language extension
|
||
#+BEGIN_SRC haskell
|
||
:set -XGeneralizedNewtypeDeriving
|
||
|
||
:{
|
||
newtype EUR = EUR { getCents :: Integer }
|
||
deriving (Eq, Ord, Show, Enum, Num, Real, Integral)
|
||
:}
|
||
|
||
EUR 200 + EUR 10
|
||
EUR 200 * 2
|
||
EUR 200
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
EUR {getCents = 210}
|
||
EUR {getCents = 400}
|
||
EUR {getCents = 200}
|
||
#+end_EXAMPLE
|
||
|
||
- Deriving Instances (via)
|
||
- Can derive the implementation of a typeclass using the implementation of the
|
||
same typeclass for a type that is /representationally equal/
|
||
- Needs ~DerivingVia~ language extension
|
||
#+BEGIN_SRC haskell
|
||
:set -XKindSignatures
|
||
:set -XDerivingVia
|
||
import Data.Kind
|
||
|
||
:{
|
||
-- Define a type with a certain structure
|
||
newtype First (f :: Type -> Type) (a :: Type) = First (f a) deriving Show
|
||
|
||
-- Define some instances
|
||
instance Semigroup (First Maybe a) where
|
||
l@(First (Just _)) <> _ = l
|
||
_ <> r = r
|
||
|
||
instance Monoid (First Maybe a) where
|
||
mempty = First Nothing
|
||
:}
|
||
|
||
(First $ Just [1,2,3]) <> (First $ Just [3,4,5])
|
||
(First $ Nothing) <> (First $ Just [3,4,5])
|
||
(First $ Nothing) <> (First $ Nothing)
|
||
|
||
:{
|
||
-- When you have a type that is representationlly equivalent
|
||
newtype MyMaybe a = MyMaybe (Maybe a)
|
||
deriving Show
|
||
-- You can derive via it those instances
|
||
deriving (Semigroup, Monoid) via (First Maybe a)
|
||
:}
|
||
|
||
(MyMaybe $ Just [1,2,3]) <> (MyMaybe $ Just [3,4,5])
|
||
(MyMaybe $ Nothing) <> (MyMaybe $ Just [3,4,5])
|
||
(MyMaybe $ Nothing) <> (MyMaybe $ Nothing)
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
First (Just [1,2,3])
|
||
First (Just [3,4,5])
|
||
First Nothing
|
||
MyMaybe (Just [1,2,3])
|
||
MyMaybe (Just [3,4,5])
|
||
MyMaybe Nothing
|
||
#+end_EXAMPLE
|
||
|
||
- Deriving Instances (any)
|
||
- Can derive a default implmentation without the need to write an empty instance
|
||
- Needs ~DeriveAnyClass~ language extension
|
||
#+BEGIN_SRC haskell
|
||
:set -XDefaultSignatures
|
||
:set -XDeriveAnyClass
|
||
|
||
:{
|
||
class Redacted a where
|
||
redacted :: a -> String
|
||
default redacted :: Show a => a -> String
|
||
redacted = show
|
||
|
||
newtype UserName = UserName String deriving (Show, Redacted)
|
||
:}
|
||
|
||
redacted $ UserName "gabrielelana"
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
<interactive>:12:52: warning: [-Wderiving-defaults]
|
||
• Both DeriveAnyClass and GeneralizedNewtypeDeriving are enabled
|
||
Defaulting to the DeriveAnyClass strategy for instantiating Redacted
|
||
• In the newtype declaration for ‘UserName’
|
||
Suggested fix:
|
||
Use DerivingStrategies
|
||
to pick a different strategy
|
||
UserName \"gabrielelana\"
|
||
#+end_EXAMPLE
|
||
|
||
- Deriving Strategies
|
||
#+BEGIN_SRC haskell
|
||
:set -XDefaultSignatures
|
||
:set -XDeriveAnyClass
|
||
:set -XDerivingStrategies
|
||
|
||
:{
|
||
class Redacted a where
|
||
redacted :: a -> String
|
||
default redacted :: Show a => a -> String
|
||
redacted = show
|
||
|
||
newtype UserName = UserName String
|
||
deriving stock Show
|
||
deriving anyclass Redacted
|
||
|
||
newtype Password = Password String
|
||
|
||
instance Show Password where
|
||
show (Password d) = "<redacted>"
|
||
|
||
newtype Secret = Secret Password
|
||
deriving newtype Show
|
||
deriving anyclass Redacted
|
||
|
||
data User = User
|
||
{ username :: UserName
|
||
, password :: Password
|
||
} deriving Show
|
||
deriving anyclass Redacted
|
||
:}
|
||
|
||
-- Will print `UserName "gabrielelana"` because Show is derived `stock` and
|
||
-- the default implementation of `Redacted` derived `anyclass` will use `Show`
|
||
redacted $ UserName "gabrielelana"
|
||
|
||
-- Will print `<redacted>` because Show is derived `newtype` from `Password`
|
||
-- which will print `<redacted>`
|
||
redacted $ Secret (Password "nosecrets")
|
||
|
||
redacted $ User (UserName "gabrielelana") (Password "nosecrets")
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
UserName \"gabrielelana\"
|
||
<redacted>
|
||
User {username = UserName \"gabrielelana\", password = <redacted>}
|
||
#+end_EXAMPLE
|
||
|
||
* Exercises
|
||
- Writing Type Classes Representing Emptiness
|
||
#+BEGIN_SRC haskell
|
||
:{
|
||
import Prelude hiding (null)
|
||
|
||
class Nullable a where
|
||
isNull :: a -> Bool
|
||
null :: a
|
||
|
||
-- Create an instance of Nullable for the following types
|
||
|
||
-- `Maybe a` where `a` is Nullable
|
||
instance Nullable a => Nullable (Maybe a) where
|
||
isNull (Just a) = isNull a
|
||
isNull Nothing = True
|
||
|
||
null = Nothing
|
||
|
||
-- `(a, b)` where `a` and `b` are Nullable
|
||
instance (Nullable a, Nullable b) => Nullable (a, b) where
|
||
isNull (a, b) = isNull a && isNull b
|
||
null = (null, null)
|
||
|
||
-- `[a]`
|
||
instance Nullable [a] where
|
||
isNull [] = True
|
||
isNull _ = False
|
||
null = []
|
||
:}
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
#+end_EXAMPLE
|
||
- Add a Default Null Test
|
||
#+BEGIN_SRC haskell
|
||
import Prelude hiding (null)
|
||
|
||
-- Given an Eq constraint on Nullable create a default implementation of isNull
|
||
:{
|
||
class Eq a => Nullable a where
|
||
isNull :: a -> Bool
|
||
isNull = (==) null
|
||
|
||
null :: a
|
||
|
||
-- Alternative with constraint only for isNull
|
||
|
||
-- class Nullable a where
|
||
-- default Eq a => isNull :: a -> Bool
|
||
-- isNull = (==) null
|
||
--
|
||
-- null :: a
|
||
|
||
instance Eq a => Nullable [a] where
|
||
null = []
|
||
:}
|
||
|
||
isNull []
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
True
|
||
#+end_EXAMPLE
|
||
- Deriving Nullable
|
||
#+BEGIN_SRC haskell
|
||
:set -XKindSignatures
|
||
:set -XDerivingVia
|
||
import Data.Kind
|
||
|
||
:{
|
||
class Nullable a where
|
||
isNull :: a -> Bool
|
||
null :: a
|
||
|
||
instance Nullable [a] where
|
||
isNull [] = True
|
||
isNull _ = False
|
||
null = []
|
||
|
||
instance Nullable (Maybe a) where
|
||
isNull Nothing = True
|
||
isNull _ = False
|
||
null = Nothing
|
||
|
||
newtype Shallow (f :: Type -> Type) (a :: Type) = Shallow (f a)
|
||
|
||
instance Nullable (Shallow Maybe a) where
|
||
isNull (Shallow Nothing) = True
|
||
isNull _ = False
|
||
null = Shallow Nothing
|
||
|
||
instance Nullable (Shallow [] a) where
|
||
isNull (Shallow []) = True
|
||
isNull _ = False
|
||
null = Shallow []
|
||
|
||
newtype Deep (f :: Type -> Type) (a :: Type) = Deep (f a)
|
||
|
||
instance Nullable a => Nullable (Deep Maybe a) where
|
||
isNull (Deep Nothing) = True
|
||
isNull (Deep (Just a)) = isNull a
|
||
null = Deep Nothing
|
||
|
||
instance Nullable a => Nullable (Deep [] a) where
|
||
isNull (Deep []) = True
|
||
isNull (Deep xs) = all isNull xs
|
||
null = Deep []
|
||
|
||
newtype W1 = W1 (Maybe [Int])
|
||
deriving Nullable via (Shallow Maybe [Int])
|
||
|
||
newtype W2 = W2 (Maybe [Int])
|
||
deriving Nullable via (Deep Maybe [Int])
|
||
|
||
newtype W3 = W3 ([Maybe Int])
|
||
deriving Nullable via (Shallow [] (Maybe Int))
|
||
|
||
newtype W4 = W4 ([Maybe Int])
|
||
deriving Nullable via (Deep [] (Maybe Int))
|
||
:}
|
||
|
||
-- Shallow Maybe [Int]
|
||
False == isNull (W1 (Just []))
|
||
True == isNull (W1 Nothing)
|
||
|
||
-- Deep Maybe [Int]
|
||
True == isNull (W2 Nothing)
|
||
True == isNull (W2 (Just []))
|
||
False == isNull (W2 (Just [1]))
|
||
|
||
-- Shallow [Maybe Int]
|
||
True == isNull (W3 [])
|
||
False == isNull (W3 [Nothing])
|
||
False == isNull (W3 [Just 1])
|
||
|
||
-- Deep [Maybe Int]
|
||
True == isNull (W4 [])
|
||
True == isNull (W4 [Nothing])
|
||
True == isNull (W4 [Nothing, Nothing])
|
||
False == isNull (W4 [Just 1])
|
||
#+END_SRC
|
||
|
||
#+RESULTS:
|
||
#+begin_EXAMPLE
|
||
True
|
||
True
|
||
True
|
||
True
|
||
True
|
||
True
|
||
True
|
||
True
|
||
True
|
||
True
|
||
True
|
||
True
|
||
#+end_EXAMPLE
|