effective-haskell/README.org
2024-02-22 16:29:11 +01:00

12 KiB
Raw Blame History

Type Classes

Summary

  • Using Ad Hoc Polymorphism with Type classes

    -- 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
    [1,2,3,4,5]
    

    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

    :{
    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]
    [1,2,3,4,5]
    
  • Composing Type Classes

    :{
    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
    :}
    
    
  • Creating Default Implementations and Minimal Definitions

    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 | (<=) #-}
  • Default Implementation only when an Instance of a Type Class is provided

    :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"
    Gabriele
    ???
    
  • 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.
    :{
    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"
    UserID "7246daaf-bf40-4528-a9fe-923cb221cab3"
    
    :{
    newtype UserID = UserID String
    
    data CustomerWithID = CustomerWithID
      { id :: UserID
      , name :: String
      , surname :: String
      , email :: String
      } deriving Show
    :}
    <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)
    
  • Deriving Instances (newtype)

    • Can derive for newtypes non stock type classes that are implemented by the underlying type
    • Needs GeneralizedNewtypeDeriving language extension
    :set -XGeneralizedNewtypeDeriving
    
    :{
    newtype EUR = EUR { getCents :: Integer }
      deriving (Eq, Ord, Show, Enum, Num, Real, Integral)
    :}
    
    EUR 200 + EUR 10
    EUR 200 * 2
    EUR 200
    EUR {getCents = 210}
    EUR {getCents = 400}
    EUR {getCents = 200}
    
  • 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
    :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)
    First (Just [1,2,3])
    First (Just [3,4,5])
    First Nothing
    MyMaybe (Just [1,2,3])
    MyMaybe (Just [3,4,5])
    MyMaybe Nothing
    
  • Deriving Instances (any)

    • Can derive a default implmentation without the need to write an empty instance
    • Needs DeriveAnyClass language extension
    :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"
    <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\"
    
  • Deriving Strategies

    :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")
    UserName \"gabrielelana\"
    <redacted>
    User {username = UserName \"gabrielelana\", password = <redacted>}
    

Exercises

  • Writing Type Classes Representing Emptiness

    :{
    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 = []
    :}
    
    
  • Add a Default Null Test

    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 []
    True
    
  • Deriving Nullable

    :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])
    True
    True
    True
    True
    True
    True
    True
    True
    True
    True
    True
    True