Search code examples
haskelltypeclassfree-monadoverlapping-instances

MonadError instance for a Free Monad


I have created a very useful Free Monad out of a sum data type. That abstracts access to a persistent data store:

data DataStoreF next = 
     Create    Asset                           ( String -> next)
  |  Read      String                          ( Asset  -> next)
  |  Update    Asset                           ( Bool   -> next)
  |  UpdateAll [Asset]                         ( Bool   -> next)
  |  Delete    Asset                           ( Bool   -> next)
  |  [...] -- etc. etc.
  |  Error     String

type DataStore = Free DataStoreF

I would like to make DataStore an instance of MonadError with the error message handled as (Free (Error str)):

instance MonadError String DataStore where
  throwError str = errorDS str
  catchError (Free (ErrorDS str)) f = f str
  catchError x _ = x

But I am running into Overlapping Instances errors.

What is the proper way to make the DataStore monad and instance of MonadError?


Solution

  • Your instance and the instance given by the library:

    instance (Functor m, MonadError e m) => MonadError e (Free m)
    

    are indeed overlapping, but this does not mean that they are incompatible. Note that the above instance is 'more general' in a sense than yours - any type which would match your instance would match this one. When one uses the OverlappingInstances extension (or with modern GHC, an {-# OVERLAP{S/PING/PABLE} #-} pragma), instances may overlap, and the most specific (least general) instance will be used.

    Without the extension, e.g. throwError "x" :: DataStore () gives the type error:

    * Overlapping instances for MonadError [Char] (Free DataStoreF)
        arising from a use of `throwError'
      Matching instances:
        instance [safe] (Functor m, MonadError e m) =>
                        MonadError e (Free m)
          -- Defined in `Control.Monad.Free'
        instance [safe] MonadError String DataStore
    

    but with the addition of a pragma

    instance {-# OVERLAPS #-} 
      MonadError String DataStore where
    

    the expression throwError "x" :: DataStore () still matches both instances, but since one is more specific than the other (the one you wrote) it is selected:

    >throwError "x" :: DataStore ()
    Free (Error "x")