Search code examples
haskellgadt

Is it possible to create a monoid instance for a GADT?


Given the following data type

{-# LANGUAGE GADTs #-}

data Response a where
  ResponseMap :: HashMap Text (Sum Int) -> Response (HashMap Text (Sum Int))
  ResponseSum :: Sum Int -> Response (Sum Int)

how would I derive a monoid instance for it? For the definition of mappend I can pattern match on the constructors

  (ResponseSum v1) `mappend` (ResponseSum v2) = undefined
  (ResponseMap v1) `mappend` (ResponseMap v2) = undefined

and combine the values easily but I don't see how I would implement mempty, or if it's indeed possible, or makes sense?


Solution

  • As you've noticed, you can't provide an instance Monoid (Response a), because you can't define mempty :: Response a. Why not? Well, mempty has to have type Response a for all a, including, say, Bool. But you can't construct a value of type Response Bool, only Response (HashMap Text (Sum Int)) and Response (Sum Int). So you won't be able to create a mempty. This isn't a problem for mappend, because you're given a Response a, so you can check which a you were given. But mempty has nothing to analyze.

    So what can you do? Well, first of all, you can provide an instance Semigroup (Response a). A semigroup is exactly a monoid without mempty, so this is exactly what you want. As of GHC 8, you can find this type class in the base package, in the module Data.Semigroup; prior to that, you need to use the semigroups package, with the same module name. Instead of mappend, it uses the binary operator (<>). So you'd have

    import Data.Semigroup
    import Data.Monoid hiding ((<>))
    
    -- ...
    
    instance Semigroup (Response a) where
      ResponseMap v1 <> ResponseMap v2 = ResponseMap $ v1 <> v2
      ResponseSum v1 <> ResponseSum v2 = ResponseSum $ v1 <> v2
    

    You can also provide specific Monoid instances for the type indices that you can construct. With FlexibleInstances, that looks like

    {-# LANGUAGE FlexibleInstances #-}
    
    instance Monoid (Response (HashMap Text (Sum Int))) where
      mempty = ResponseMap mempty
      mappend = (<>)
    
    instance Monoid (Response (Sum Int)) where
      mempty = ResponseSum mempty
      mappend = (<>)
    

    Now, for the cases where you do know what the unit is, you have a Monoid instance.