Каков наилучший способ управления ресурсами в стеке монады, например ExceptT a IO?

К лучшему или к худшему, Haskell популярная библиотека Servant сделала ее обычной для запуска кода в стеке монадного трансформатора, ExceptT err IO. Брат владельца-владельца-обработчика ExceptT ServantErr IO. Как утверждают многие, это несколько неудобная монада для работы, поскольку существует множество способов для развёртывания: 1) через обычные исключения из IO в базе, или 2), возвращая Left.

Как Ed Kmett exceptions библиотека полезно разъясняет:

Моноды на основе продолжения и стеки, такие как ErrorT e IO, которые обеспечивают множественные режимы отказа, являются недопустимыми экземплярами этого класса [MonadMask].

Это очень неудобно, так как MonadMask дает нам доступ к полезной [полиморфной версии] функции bracket для выполнения управления ресурсами (не утечка ресурсов из-за исключения и т.д.). Но в монаде-слуге Handler мы не можем его использовать.

Я не очень хорошо знаком с этим, но некоторые люди говорят, что решение состоит в использовании monad-control, и многие библиотеки-партнеры, такие как lifted-base и lifted-async, чтобы предоставить вашему монаде доступ к инструментам управления ресурсами, например bracket (возможно, это работает и для ExceptT err IO и для друзей?).

Однако, кажется, что monad-control проигрывает в сообществе, но я не могу сказать, какой будет альтернатива. Даже недавняя библиотека safe-exceptions Snoyman использует библиотеку Kmett exceptions и избегает monad-control.

Может ли кто-нибудь прояснить текущую историю для таких людей, как я, которые пытаются вспахать наш путь в серьезное использование Haskell?

Ответы

Ответ 1

Вы можете работать в IO, возвращать значение типа IO (Either ServantErr r) в конце и обернуть его в ExceptT, чтобы он соответствовал типу обработчика. Это позволит вам использовать bracket обычно в IO. Одна из проблем с этим подходом заключается в том, что вы теряете "автоматическое управление ошибками", которое предоставляет ExceptT. То есть, если вы потерпите неудачу в середине обработчика, вам нужно будет выполнить явное сопоставление шаблонов на Either и тому подобное.


Вышеупомянутое в основном переопределяет экземпляр MonadTransControl для ExceptT, который

instance MonadTransControl (ExceptT e) where
    type StT (ExceptT e) a = Either e a
    liftWith f = ExceptT $ liftM return $ f $ runExceptT
    restoreT = ExceptT

monad-control отлично работает при подъеме функций, таких как bracket, но у него есть нечетные угловые случаи с функциями вроде следующего (взято из это сообщение в блоге):

import Control.Monad.Trans.Control

callTwice :: IO a -> IO a
callTwice action = action >> action

callTwice' :: ExceptT () IO () -> ExceptT () IO ()
callTwice' = liftBaseOp_ callTwice

Если мы перейдем к callTwice' к действию, которое что-то печатает и сбой сразу после

main :: IO ()
main = do
    let printAndFail = lift (putStrLn "foo") >> throwE ()
    runExceptT (callTwice' printAndFail) >>= print  

Он печатает "foo" два раза в любом случае, даже если наша интуиция говорит, что он должен остановиться после первого выполнения действия.


Альтернативный подход - использовать библиотеку resourcet и работать в монаде ExceptT ServantErr (ResourceT IO) r. Вам нужно будет использовать функции resourcet, такие как allocate вместо bracket, и адаптировать монаду в конце, например:

import Control.Monad.Trans.Resource
import Control.Monad.Trans.Except

adapt :: ExceptT ServantErr (ResourceT IO) r -> ExceptT err IO r 
adapt = ExceptT . runResourceT . runExceptT

или как:

import Control.Monad.Morph

adapt' :: ExceptT err (ResourceT IO) r -> ExceptT err IO r 
adapt' = hoist runResourceT

Ответ 2

Моя рекомендация: сохраните свой код в IO вместо ExceptT и оберните каждую функцию обработчика в ExceptT . try.