MonadError экземпляр для свободной монады

Я создал очень полезную Free Monad из типа данных sum. Это абстрагирует доступ к постоянному хранилищу данных:

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

Я хотел бы сделать DataStore экземпляр MonadError с сообщением об ошибке, обработанным как (Free (Error str)):

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

Но я сталкиваюсь с ошибками перекрывающихся экземпляров.

Как правильно сделать DataStore монада и экземпляр MonadError?

2 ответа

Решение

Ваш экземпляр и экземпляр, данный библиотекой:

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

действительно перекрываются, но это не значит, что они несовместимы. Обратите внимание, что приведенный выше пример является "более общим" в некотором смысле, чем ваш - любой тип, который будет соответствовать вашему экземпляру, будет соответствовать этому. Когда кто-то использует OverlappingInstances расширение (или с современным GHC, {-# OVERLAP{S/PING/PABLE} #-} pragma), экземпляры могут перекрываться, и будет использоваться наиболее конкретный (наименее общий) экземпляр.

Без расширения, например throwError "x" :: DataStore () выдает ошибку типа:

* 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

но с добавлением прагмы

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

выражение throwError "x" :: DataStore () по-прежнему совпадает с обоими экземплярами, но так как один более конкретный, чем другой (тот, который вы написали), он выбран:

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

Free тип уже обеспечивает MonadError экземпляр для всех бесплатных монад:

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

Когда ты пишешь type DataStore = ...Вы просто определяете псевдоним типа, который в основном является макросом уровня типа. Все виды использования DataStore тип заменяется его определением. Это означает, что с помощью DataStore неотличим от использования Free DataStoreF напрямую, поэтому, когда вы делаете это:

instance MonadError String DataStore where { ... }

... вы на самом деле делаете это:

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

... и это противоречит определенному выше случаю.

Чтобы обойти это, вы должны определить newtype создать совершенно новый тип, который может иметь свои экземпляры, не относящиеся к тем, которые определены в Free, Если вы используете GeneralizedNewtypeDeriving расширение, вы можете избежать много шаблонного, который в противном случае потребовался бы отдельным newtype:

{-# LANGUAGE GeneralizedNewtypeDeriving -}

data DataStoreF next = ...

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

instance MonadError String DataStore where { ... }

Это должно избежать проблемы перекрывающихся экземпляров без необходимости выписывать все Functor, Applicative, а также Monad экземпляры вручную.

Другие вопросы по тегам