What can you do that is useful with Haskell Type Classes?

In adition to chepner's explanation of the usefulness of type classes, here are some more practical examples of type classes outside of Prelude:

  • Arbitrary from QuickCheck (A QuickCheck Tutorial: Generators).
  • Example from Hspec, which is similar but not exactly equivalent.
  • WithLog (or rather, HasLog) from co-log.
  • SafeCopy is another serialization class, but with different constraints than Aeson's FromJSON, ToJSON, since it also deals with data format migrations.

Since there's a whole design space for using type classes in different ways, here are some more thoughts:

  • The Has Type Class Pattern: Tutorial 1, Tutorial 2, and the package data-has.

  • An interesting library related to QuickCheck is Hedgehog, which does away with type classes for a strong reason (tutorial and generally an eye-opener). So there may be lots of reasons to use and not use type classes; often there simply already exists exactly the type class you're looking for.

  • It may be worth to read Gabriel Gonzalez' Scrap Your Type Classes which highlights some of the downsides of the uses of type classes. As the blog post starts with, his "opinion on type classes has mellowed since I wrote this post, but I still keep it around as a critique against the excesses of type classes."

If there is a reason to use type classes, does anyone have a good project for a beginner to practice using them or some good form guides for them?

It really depends on whether you want to define a type class, or just define type class instances for existing type classes, use an existing type class in base, or use some type class in an extended library.

It can be fun to define type class instances for things that are Monoid or Semigroup (tutorial). It can also be fun to define your own ToJSON and FromJSON instances for some JSON data format that you might find interesting (tutorial).


The definition

class Foo a where
    bar :: a -> a -> Bool

is very simililar to

class Eq a where
    (==) :: a -> a -> Bool 
    (/=) :: a -> a -> Bool 

and here is when you can find how useful can it be:

Imagine you've got slugs, and want to know if they can procreate, and there is a rare species with hermaphrodite type, you can use your typeclass:

data Slug = M | F | H

class Foo a where
    bar :: a -> a -> Bool

instance Foo Slug where
   bar H _ = True
   bar _ H = True
   bar F M = True
   bar M F = True
   bar _ _ = False  

Or, with temperatures: You want to know if mixing water will get you warm water:

data Temp = Cold | Hot | Warm

instance Foo Temp where
   bar Warm _    = True
   bar _ Warm    = True
   bar Hot Cold  = True
   bar Cold Hot  = True
   bar _ _       = False  

So, that typeclass now could be named sort of "Mixable", and the method, "mix", and it would be less confusing to read for type Slug and Temperature.

Now, if you want to watch it in action with some example, I can came up now with something like...

mix :: Foo a => [a] -> [a] -> [Bool]
mix xs ys = zipWith bar xs ys

$>  mix [M,M,H,F] [F,F,F,F]
=> [True,True,True,False]

but there is a restriction with mix, you can just mix Mixable things. so if you do:

mix [1,1] [2,2]

will break:

9:1: error:
    • No instance for (Foo Bool) arising from a use of ‘mix’
    • In the expression: mix [True, True] [False, True]
      In an equation for ‘it’: it = mix [True, True] [False, 

And that means, that you can organize you data types to satisfy the mix function according its structure, or your needs.

Level 2:

What if you want a default implementation for Slug and Temp? Because you saw they where similar, so you could do:

class (Bounded a, Eq a) => Mixable a where
    mix :: a -> a -> Bool
    mix e1 e2 = e1 /= e2 || any (\x -> x /= minBound && x /= maxBound) [e1, e2]

data Slug = F | H | M deriving (Bounded, Eq, Show)
data Temp = Cold | Warm | Hot deriving (Bounded, Eq, Show)

instance Mixable Slug
instance Mixable Temp

mixAll :: Mixable a => [a] -> [a] -> [Bool]
mixAll xs ys = zipWith mix xs ys

main = do
  putStrLn $ show (mixAll [F,F,F,M,M,M,H] [F,M,H,M,F,H,H])
  putStrLn $ show (mixAll [Cold,Cold,Cold,Hot,Hot,Hot,Warm] [Cold,Hot,Warm,Hot,Cold,Warm,Warm])

[False,True,True,False,True,True,True]

[False,True,True,False,True,True,True]


Type classes provide ad hoc polymorphism, as opposed to parametric polymorphism: a function does not need to be defined the same way (or at all) for each type. In addition, it does so in an open fashion: you don't need to enumerate all the types that implement the class when you define the class itself.

Some prominent examples of non-standard type classes are the various MonadFoo classes provided by mtl (monad transformer library), ToJSON and FromJSON provided by the aeson library, and IsString, which makes the OverloadedString extension work.


Without type classes, you can define a function that works for a single argument type

foo :: Int -> Int

or one that works for all argument types

foo :: a -> Int

The only way to work for some subset of types is to use a sum type

foo :: Either Int Bool -> Int

but you can't later define foo for Float without changing the type of foo itself

foo :: Either Int (Either Bool Float) -> Int

or

data IntBoolFloat = T1 Int | T2 Bool | T3 Float
foo :: IntBoolFloat -> Int

either or which will be cumbersome to work with.

Typeclasses let you work with one type at a time, and let you add new types in a nonintrusive fashion.

class ToInt a where
    foo :: a -> Int

instance ToInt Int where foo = id
instance ToInt Bool where
    foo True = 1
    foo False = 2
instance ToInt Float where
    foo x = 3  -- Kind of pointless, but valid

An instance of ToInt can be defined anywhere, although in practice it's a good idea for it to be defined either in the module where the class itself is defined, or in the module where the type being instantiated is defined.


Underneath the hood, a method (a function defined by a type class) is essentially a mapping of types to functions. The TypeApplications extension makes that more explicit. For example, the following are equivalent.

foo True == 1
foo @Bool True == 1 -- foo :: ToInt a => a -> Int, but foo @Bool :: Bool -> Int

Tags:

Haskell