2016-08-05 5 views
3

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

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?

+0

Вы можете обернуть его в «новый тип» и сами контролировать экземпляры. Я не думаю, что есть более чистый способ сделать это. Экземпляры для 'FreeT' предполагают, что вы не будете предоставлять ни один из классов mtl, предоставляемых другими трансформаторами. – Cirdec

+0

Как это отличается от последней строки первого блока кода? –

+1

@ JohnF.Miller В вашем примере «DataStore» - это просто псевдоним типа, а не «новый тип», который автоматически предоставляет вам все экземпляры Free'. Однако, если вы сделаете это «newtype», вы можете использовать «GeneralizedNewtypeDeriving», чтобы выбрать, какие экземпляры вы хотите наследовать, и вы можете определить свой собственный экземпляр «MonadError». –

ответ

2

Ваш экземпляр и экземпляр дается в библиотеке:

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

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

Без расширения, например.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") 
+0

К будущим читателям: ответ Алексиса Кинга, вероятно, является более подходящим выбором в идеальном мире, и его следует серьезно рассмотреть, прежде чем использовать несколько подозрительное расширение языка, например '{- # OVERLAPS # -}', но это так элегантно и быстро адресовало мои вопрос в уже обширной базе кода, которую я выбрал в качестве моего принятого ответа. Спасибо вам (от 2016-08-06) от вас, кто ответил. –

5

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

+0

Что делает встроенный экземпляр? Я посмотрел на него, но я не могу выполнить требование, чтобы «DataStoreF» был «MonadError», потому что на самом деле это не монада. –

+0

@ JohnF.Miller Экземпляр по умолчанию говорит, что 'Free' является [monad transformer] (https://hackage.haskell.org/package/transformers-0.5.2.0/docs/Control-Monad-Trans-Class.html), что происходит, чтобы сохранить «MonadError». –

+1

@ JohnF.Miller Правильно, это работает только в том случае, если 'DataStoreF' уже является монадой, что приводит к поражению цели общего использования для использования' Free'. Это не большая проблема, хотя, учитывая, что «новый тип» работает так же хорошо. –