What Are "Free" SomeTypeClass
Types
When we first introduced type classes, we explained that they are an encapsulation of 2-3 things:
- (always) A definition of 1 or more functions/values' type signatures
- (almost always) Laws to which a concrete type's implementation of said type class must adhere
- (frequently) Functions that a type obtains for free once the core defintion/values are implemented
Moreover, some type classes combine two or more type classes together
Thus, SomeTypeClass
isn't so much a 'thing' as much as it is an expectation. We don't say that f
is a SomeTypeClass
(for it could implement it in various ways); rather, we are really saying that f
has an instance that implements SomeTypeClass
's specialFunction
function in such a way that it adheres to SomeTypeClass
's laws. As we saw from the MTL folder, even StateT
, a newtyped function, can be called a Functor
because it meets all of these requirements.
However, whenever we had a type that we wanted to use in a Functor
-like way, we needed to define its Functor
instance before we could use it in that way. In other words, we have to write a lot of boilerplate code.
What if we could grant Functor
-like capabailities for any type without implementing such an instance? That is the idea behind "free" type classes.
Essentially, a "free" TypeClassName
is a box-like type, Wrapper
, that grants TypeClassName
-capabilities to some other type, A
, by providing the necessary structure for implementing a law-abiding TypeClassName
instance for Wrapper
.
In short, to create a "free" SomeTypeClass
, we do 2 things:
- Translate the type class into a higher-kinded type
- Do the following for each of the type class' functions, starting with the easiest function:
- Translate one type class' function into a constructor for the new type
- Try to implement all required instances using the constructor
- Fix problems that arise
A "Free" Monoid
When we look at Monoid
, we see this type class:
class Monoid a where {-
append :: a -> a -> a -- include Semigroup's function -}
mempty :: a
-- "hello" <> "world" == "helloworld"
-- "hello" <> mempty == "hello"
-- mempty <> "hello" == "hello"
Let's follow the instructions from above: First, we'll translate the type class into a data type that can take any type:
data FreeMonoid a
Second and starting with the easier function mempty
, we'll translate it into a constructor for FreeMonoid
. mempty
is easy, since it translates into a placeholder constructor:
data FreeMonoid a
= Mempty
instance Semigroup (FreeMonoid a) where
append a Mempty = a
append Mempty a = a
instance Monoid (FreeMonoid a) where
mempty = Mempty
append
is a bit harder. We need to store a value of type a
, so we can try this:
data FreeMonoid a
= Mempty
| Append a
However, if we try to implement this as (Append a1) <> (Append a2)
, we can't combine a1
and a2
. Rather, we need to store both an a1
and an a2
in a single Append
:
data FreeMonoid a
= Mempty
-- a1 a2
| Append a a
-- since `Mempty` is our placeholder instance, we can use it
-- to fill the a2's spot
instance Semigroup (FreeMonoid a) where
append Mempty Mempty = Mempty
append a Mempty = a
append Mempty a = a
append (Append a1 Mempty) (Append a2 Mempty) = Append a1 a2
-- Works!
(Append a1 Mempty) <> (Append a2 Mempty) == Append a1 a2
-- ...well, not quite!
(Append a1 a2) <> (Append a3 Mempty) == -- ???
Our previous solution doesn't work either. If the failure case above is just another append, we get something like this:
((Append a1 Mempty) <> (Append a2 Mempty)) <> (Append a3 Mempty)
Rather than defining a second a
for Append
, what if we nested the types together? This approach makes our code finally work:
data FreeMonoid a
= Mempty
-- a1 Mempty / Append a
| Append a (FreeMonoid a )
instance Semigroup (FreeMonoid a) where
append Mempty Mempty = Mempty
append a Mempty = a
append Mempty a = a
append (Append a memptyOrAppend) otherAppend =
Append a (memptyOrAppend <> otherAppend)
instance Monoid (FreeMonoid a) where
mempty = Mempty
The above code is the exact same thing as a familiar data type, List
:
data List a
= Nil
| Cons a (List a)
instance Semigroup (List a) where
append Nil Nil = Nil
append a Nil = a
append Nil a = a
append (Cons head tail) otherList =
Cons head (tail <> otherList)
instance Monoid (List a) where
mempty = Nil
Thus, we say that List
is a "free" monoid because by wrapping some type (e.g. Fruit
) into a List
, we get a monoid instance for Fruit
for free:
data Fruit
= Apple
| Orange
| Banana
(Cons (Apple Nil)) <> (Cons Banana Nil) == Cons (Apple (Cons Banana Nil))
This idea can be useful for when we have types that can't implement specific type classes.