I understand that type classes are very useful for organizing data and for type checking etc., but other than what is already included in prelude, is there ever a need to define your own class?
Under almost any circumstance one could just define a data or newtype and get almost the same effect anyways. Using the built in "Ord", "Eq", "Show", and others seems to be enough to do anything you would want to do with classes.
When I looked up projects for classes in Haskell I get a lot of example classes like so:
class Foo a where
bar :: a -> a -> Bool
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?
A typeclass is a sort of interface that defines some behavior. If a type is a part of a typeclass, that means that it supports and implements the behavior the typeclass describes. A lot of people coming from OOP get confused by typeclasses because they think they are like classes in object oriented languages.
An instance of a class is an individual object which belongs to that class. In Haskell, the class system is (roughly speaking) a way to group similar types. (This is the reason we call them "type classes"). An instance of a class is an individual type which belongs to that class.
In Haskell, every statement is considered as a mathematical expression and the category of this expression is called as a Type. You can say that "Type" is the data type of the expression used at compile time. To learn more about the Type, we will use the ":t" command.
Type Classes are a language mechanism in Haskell designed to support general overloading in a principled way. They address each of the concerns raised above. They provide concise types to describe overloaded functions, so there is no expo- nential blow-up in the number of versions of an overloaded function.
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
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]
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With