Skip to content
AgileLogics
6 min read

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 describe runs 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 Sum and Product newtypes choose which.
  • foldMap f maps every element to a monoid and combines the results.
  • Ordering is a monoid: compare a b <> compare c d compares 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)