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'sFromJSON
,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