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
экземпляры вручную.