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
?
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.
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")
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With