blog-static/content/blog/haskell_newtype.md

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
Haskell
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 of m, like a, b, and c, the following holds:

    (a <> b) <> c == a <> (b <> c)
    
  • An neutral element under (<>), called mempty. It being the neutral element means that using it with (<>) doesn't do anything. For any a :: 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.

Booleans