Defining Your Own Type Classes
Prerequisites: Type classes
A class of your own
class Describable a where
describe :: a -> String
name :: a -> String
name _ = "thing"
data Dog = Dog
data Car = Car {brand :: String}
instance Describable Dog where
describe _ = "a loyal companion"
name _ = "dog"
instance Describable Car where
describe c = "a car made by " ++ brand c
instance Describable Bool where
describe True = "yes"
describe False = "no"
introduce :: Describable a => a -> String
introduce x = "This " ++ name x ++ " is " ++ describe x
main :: IO ()
main = do
putStrLn (introduce Dog)
putStrLn (introduce (Car "Volvo"))
putStrLn (introduce True)
This dog is a loyal companion
This thing is a car made by Volvo
This thing is yes
- A class declares method signatures and optional default
implementations (
name). - Any type — even a built-in one like
Bool— can become an instance. - Which
describeruns is chosen at compile time from the type.
Semigroup and Monoid
Two small classes from the standard library show up everywhere.
A Semigroup has an associative way to combine two values, <>; a
Monoid also has an "empty" value, mempty:
import Data.Monoid (Sum (..), Product (..), Any (..))
main :: IO ()
main = do
print ([1, 2] <> [3], "ab" <> "cd")
print (mconcat ["con", "cat", "enate"])
print (getSum (foldMap Sum [1 .. 10]))
print (getProduct (foldMap Product [1 .. 5]))
print (getAny (foldMap (Any . even) [1, 3, 5, 6]))
print (Just [1] <> Nothing <> Just [2, 3])
print (compare 1 2 <> compare 3 3)
([1,2,3],"abcd")
"concatenate"
55
120
True
Just [1,2,3]
LT
- Numbers can be combined in more than one way (adding or multiplying),
so the
SumandProductnewtypes choose which. foldMap fmaps every element to a monoid and combines the results.Orderingis a monoid:compare a b <> compare c dcompares by a first, then by c and d — a neat trick for multi-key sorting.
Your own Monoid
data Stats = Stats {count :: Int, total :: Double, biggest :: Double}
deriving (Show)
instance Semigroup Stats where
Stats c1 t1 b1 <> Stats c2 t2 b2 = Stats (c1 + c2) (t1 + t2) (max b1 b2)
instance Monoid Stats where
mempty = Stats 0 0 (-1 / 0)
single :: Double -> Stats
single x = Stats 1 x x
main :: IO ()
main = do
let s = foldMap single [4, 8, 15, 16, 23, 42]
print s
print (total s / fromIntegral (count s))
Stats {count = 6, total = 108.0, biggest = 42.0}
18.0
Because combining is associative, Stats for separate chunks of data can
be computed independently (even in parallel) and merged at the end.
Practice
Define a class Shape a with area :: a -> Double and
perimeter :: a -> Double, and instances for Square and Circle
types.
Show solution
class Shape a where
area :: a -> Double
perimeter :: a -> Double
newtype Square = Square Double
newtype Circle = Circle Double
instance Shape Square where
area (Square s) = s * s
perimeter (Square s) = 4 * s
instance Shape Circle where
area (Circle r) = pi * r * r
perimeter (Circle r) = 2 * pi * r
main :: IO ()
main = do
print (area (Square 3), perimeter (Square 3))
print (area (Circle 1), perimeter (Circle 1))
(9.0,12.0)
(3.141592653589793,6.283185307179586)