3.2 KiB
title, date, expirydate, tags, draft
| 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.