3.2 KiB
title | date | expirydate | tags | draft | |
---|---|---|---|---|---|
Multiple Typeclass Instances using Newtype in Haskell | 2022-04-28T23:09:42-07:00 | 2022-04-28T23:09:42-07:00 |
|
true |
And I have known the eyes already, known them all—
The eyes that fix you in a formulated phrase . . .
It sometimes feels like the data types that we work with are simply
too locked down through their typeclass instances. A type might have multiple
law-abiding implementations of the required methods, and yet only one implementation can be used
in an instance
declaration. First, let's take a look at some type classes and types
for which this is true, starting with Monoid
.
The Monoid
Type Class
Recall that for a type m
with Monoid m
, the following things are available:
-
An associative binary operation,
(<>)
. Associativity means that for any three elements ofm
, likea
,b
, andc
, the following holds:(a <> b) <> c == a <> (b <> c)
-
An neutral element under
(<>)
, calledmempty
. It being the neutral element means that using it with(<>)
doesn't do anything. For anya :: m
, we can (with minor notation abuse) write the following:a <> mempty == mempty <> a == a
This is a pretty broad specification, actually. Many types have multiple different
operations that satisfy the Monoid
laws. Let's take a look at a few of them,
starting with integers.
Integers
Addition is an associative binary operation. Furthermore, it's well-known that adding zero to a
number leaves that number intact: 0+n = n + 0 = n
. So we might define a Monoid
instance for
numbers as follows. Note that we actually provide (<>)
via the Semigroup
class,
which just requires the associative binary operation, and serves as a superclass for Monoid
.
instance Semigroup Int where
(<>) = (+)
instance Monoid Int where
mempty = 0
Cool and good. But hey, there are other binary operations on integers! What about
multiplication? It is also associative, and again it is well-known that multiplying
anything by one leaves that number as it was: 1\*n = n\*1 = n
. The corresponding
Monoid
instance would be something like the following:
instance Semigroup Int where
(<>) = (*)
instance Monoid Int where
mempty = 1
But we can't have both. Haskell yells at us:
Duplicate instance declarations:
instance Semigroup Int -- Defined at test.hs:1:10
instance Semigroup Int -- Defined at test.hs:7:10
Duplicate instance declarations:
instance Monoid Int -- Defined at test.hs:4:10
instance Monoid Int -- Defined at test.hs:10:10
Okay, so we can have at most one. But that's not good.
Fortunately, thanks to the Num
instance for Int
, we get
functions that are pretty much the same as fold
, except
specialized to multiplication and addition:
fold :: (Foldable t, Monoid m) => t m -> m
product :: (Foldable t, Num a) => t a -> a
sum :: (Foldable t, Num a) => t a -> a
This takes care of most of the uses we have for (+)
and (*)
;
it does, however, prevent us from using Int
with MonadWriter
.