4

I am trying to make a tic-tac-toe game, and I decided to construct types for cells (elements of the board) and the board as follows:

data Cell  = X | O deriving (Show, Eq)
type Board = [[Maybe Cell]]

Here, Nothing represents an empty cell, (Just X) and (Just O) represent cells filled with X and O respectively.

I would like to define (Maybe Cell) as a monoid as follows:

instance Monoid (Maybe Cell) where
  mempty             = Nothing
  mappend Nothing x  = x
  mappend (Just x) _ = (Just x)

And Board as another monoid with

instance Monoid Board where
  mempty = [[Nothing, Nothing, Nothing]
           ,[Nothing, Nothing, Nothing]
           ,[Nothing, Nothing, Nothing]]
  mappend = zipWith (zipWith mappend) 
  -- where the right-hand-side mappend is the addition on (Maybe Cell)

I know I could implement this absolutely without monoids, but I'm trying to explore the field, and it's just a really neat way to write it.

The problem that I get is that a Maybe monoid instance is already defined in GHC.Base as follows:

instance Semigroup a => Monoid (Maybe a)

This has a very different definition than what I want, but it causes duplicate instance declarations, so I can't just ignore it.

What I'm trying to do is to hide the Monoid instance of (Maybe a) from GHC.Base to avoid duplicate instances. I tried searching a lot for that, but couldn't really find a way to hide that. I can't hide all of Monoid or all of Semigroup, because I need their functions, but I need to hide this specific instance declaration. Could anyone help me with that?

NOTE: I'm using FlexibleInstances.

Jad Issa
  • 65
  • 1
  • 6
  • 4
    You can't hide `instance` declarations. What you can do though is use a `newtype` to "wrap" the underlying types. This is done quite a lot even in the standard libraries - for example the `Sum` and `Product` types are both wrappers for `Int` (actually any instance of `Num`), with different `Monoid` instances. – Robin Zigmond Feb 22 '19 at 21:25
  • oh, and your `Monoid` instance for `Maybe Cell` already exists as a `newtype` wrapper: http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Monoid.html#t:First – Robin Zigmond Feb 22 '19 at 21:27
  • @RobinZigmond Wow! That was fast! It is correct; `First` solved the problem altogether. Thanks a lot! – Jad Issa Feb 22 '19 at 21:40

1 Answers1

7

I standard Haskell, class instances are always “completely global” – if a type has an instance for a given class somewhere, then this instance is used everywhere.

So, if you want to define a separate instance, you need to either have a different class – usually not practical, including in your example – or a different type, which is usually not a problem. In fact Haskell has a dedicated keyword for this kind of thing, newtype. You simply change type Board = [[Maybe Cell]] to

newtype Board = Board [[Maybe Cell]]

and then

instance Semigroup Board where
  Board l <> Board r = Board $ zipWith (zipWith mappend) l r
instance Monoid Board where
  mempty = Board [[Nothing, Nothing, Nothing]
                 ,[Nothing, Nothing, Nothing]
                 ,[Nothing, Nothing, Nothing]]
  mappend = (<>)

Likewise, instead of Maybe Cell you should use another type that has the suitable Monoid instance. That one actually exists already in the base library, but it's not really necessary: you can just make a semigroup (not monoid!) instance for Cell itself which represents the left-bias, then Maybe will (since GHC-8.4) automatically have the desired behaviour.

instance Semigroup Cell where
  a <> _ = a

It has actually been proposed to relax this, allowing locally-selected instances, in a paper presented at the 2018 Haskell Symposium.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • TIL `First` is deprecated – luqui Feb 23 '19 at 11:13
  • @luqui ah good point, [`Semigroup.First`](http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Semigroup.html#t:First) should be used instead of [`Monoid.First`](http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Monoid.html#t:First). – leftaroundabout Feb 23 '19 at 12:21