4

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?

John F. Miller
  • 26,961
  • 10
  • 71
  • 121
  • You can wrap it in a `newtype` and control the instances yourself. I don't think there's a cleaner way to do it. The instances for `FreeT` assume you won't be providing any of the mtl classes provided by other transformers. – Cirdec Aug 05 '16 at 22:50
  • How is this different from the last line of the first code block? – John F. Miller Aug 05 '16 at 22:56
  • 1
    @JohnF.Miller In your example, `DataStore` is just a type alias, not a `newtype`, which gives you all of `Free`’s instances automatically. However, if you make it a `newtype` instead, you can use `GeneralizedNewtypeDeriving` to pick and choose which instances you want to inherit, and you can define your own `MonadError` instance. – Alexis King Aug 05 '16 at 22:59
  • 1
    You could just enable overlapping instances (or add the `{-# OVERLAPS #-}` pragma to your instance, if you have a sufficiently recent GHC version). With this, e.g. `throwError undefined :: DataStore ()` would use your instance. Unless you are for some reason unwilling to use this feature, this is certainly the simplest solution (the addition of a single line fixes your problem). – user2407038 Aug 05 '16 at 23:52
  • @user2407038 that solved my problem without any large rewrite! (We'll see if the enchanted brooms flood my basement later) If you put write this as an answer, I will accept it. – John F. Miller Aug 06 '16 at 00:16
  • 1
    By the way, your definition of `catchError` seems dubious to me; I'd expect that `catchError` would also catch errors that aren't the first step taken by the action. – Reid Barton Aug 06 '16 at 05:22

2 Answers2

5

The Free type already provides a MonadError instance for all free monads:

instance (Functor m, MonadError e m) => MonadError e (Free m) where { ... }

When you write type DataStore = ..., you are simply defining a type alias, which is basically a type-level macro. All uses of the DataStore type are replaced with its definition. This means that using DataStore is indistinguishable from using Free DataStoreF directly, so when you do this:

instance MonadError String DataStore where { ... }

…you are actually doing this:

instance MonadError String (Free DataStoreF) where { ... }

…and that conflicts with the instance defined above.

To circumvent that, you should define a newtype to produce an entirely fresh type that can have its own instances on it, unrelated to the ones defined on Free. If you use the GeneralizedNewtypeDeriving extension, you can avoid a lot of the boilerplate that would otherwise be required by a separate newtype:

{-# LANGUAGE GeneralizedNewtypeDeriving -}

data DataStoreF next = ...

newtype DataStore a = DataStore (Free DataStoreF a)
  deriving (Functor, Applicative, Monad)

instance MonadError String DataStore where { ... }

This should avoid the overlapping instance problem without the need to write out all the Functor, Applicative, and Monad instances manually.

Alexis King
  • 43,109
  • 15
  • 131
  • 205
  • What does the builtin instance do? I looked at it, but I cannot fulfill the requirement that `DataStoreF` be an `MonadError` because it is in fact not a monad. – John F. Miller Aug 05 '16 at 23:12
  • @JohnF.Miller The default instance just says that `Free` is a [monad transformer](https://hackage.haskell.org/package/transformers-0.5.2.0/docs/Control-Monad-Trans-Class.html) that happens to preserve `MonadError`. – Benjamin Hodgson Aug 05 '16 at 23:14
  • 1
    @JohnF.Miller Right, it only works if `DataStoreF` is already a monad, which sort of defeats the whole purpose of the common use-case for using `Free`. It’s not much of an issue, though, given that a `newtype` works just as well. – Alexis King Aug 05 '16 at 23:15
  • @AlexisKing, ok, I'm just not looking forward to adding the `newtype` annotation to three different interpreters + helper functions * 16 instructions each. – John F. Miller Aug 05 '16 at 23:22
  • @JohnF.Miller Yeah, it’s a little syntactically inconvenient. In general, I would avoid type aliases in Haskell—they tend to cause more problems than they solve, and they don’t really add any safety. Almost always prefer `newtype`s. – Alexis King Aug 05 '16 at 23:25
  • @JohnF.Miller You shouldn't have to unpack the `newtype` for every case of your base functor! If your interpreters work [compositionally](https://en.wikipedia.org/wiki/Principle_of_compositionality) you should be able to use [`iterM`](https://hackage.haskell.org/package/free-4.12.4/docs/Control-Monad-Free.html#v:iterM) to tear down your free monad (a bit like [`cata`](http://stackoverflow.com/documentation/haskell/2984/recursion-schemes/10137/folding-up-a-structure-one-layer-at-a-time)) and write an algebra that doesn't worry about the free monad. – Benjamin Hodgson Aug 05 '16 at 23:50
  • 1
    Actually the requirement is that the base monad (here `Identity`) needs to be an instance of `MonadError`. OP might be able to use a free monad transformer on an error monad rather than defining the `Error` action as part of `DataStoreF`. – Reid Barton Aug 06 '16 at 00:18
2

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")
user2407038
  • 14,400
  • 3
  • 29
  • 42
  • To future readers: Alexis King's answer is probably the more appropriate choice in an ideal world, and should be strongly considered before using a somewhat suspect language extension like `{-# OVERLAPS #-}`, but this so elegantly and quickly addressed my issue in an already extensive code base that I have chosen it as my accepted answer. Thank you to both (as of 2016-08-06) of you who responded. – John F. Miller Aug 06 '16 at 19:28