#haskell
#haskell
Вопрос:
Возможно ли иметь монаду WriterT, которая может делиться своими частично собранными tell
записями в случае исключения? Если я try
за пределами runWriterT
w
, кажется, отброшен. Если я попытаюсь try
проникнуть внутрь, мне кажется, что мне нужно MonadUnliftIO
. MonadUnliftIO
похоже, это может мне помочь, но в этом пакете говорится, что он способен только разблокировать монадические контексты, а не монадическое состояние, которым, я полагаю, является Writer. Кто-нибудь делал это с помощью Writer или чего-то подобного?
Пример псевдокода:
x <- runWriterT $ do
result <- try $ do
tell "a"
tell "b"
error "c"
tell "d"
case result of
Left e -> Just e
Right a -> Nothing
x `shouldBe` (Just "c", "ab")
Комментарии:
1. Попробуйте поместить преобразователь исключений `выше» преобразователя записи:
ExceptT e (WriterT w m) a
, и вам нужно будет запустить исключение перед writer:runWriterT (runExceptT ...)
.
Ответ №1:
Ну, ваш код использует error
. С моральной точки зрения, все ставки отменяются error
, потому что это означает ошибку в вашей программе больше, чем что-либо еще. Тот факт, что IO
он может перехватывать созданные им исключения, на самом деле является просто интересной причудой. Поэтому, если вам нужно такое поведение, действительно лучше использовать надлежащий преобразователь монад исключения, как рекомендует @Li-yaoXia.
-- see Control.Monad.Except
action :: (MonadExcept String m, MonadWriter String m) =>
m ()
action = do tell "a"
tell "b"
throwError "c"
tell "d"
-- run action and massage it into your format
yourOutput :: (Maybe String, String)
yourOutput = runWriter $ fmap (either Just (const Nothing)) $ runExceptT actions
Что касается того, почему error
на самом деле не может работать (по крайней мере, в хорошем смысле), подумайте, что error _ :: WriterT w m a
на самом деле означает. error _ :: Int
означает «здесь должно быть число, но вместо этого просто ошибка». WriterT w m a
это тип программы; тип программ, которые ведут журнал типа w
, выполняют некоторые другие действия ( m
) и возвращают a
. Следовательно, не означает «программа, которая выдает исправляемую ошибку, сохраняя журнал типа», это означает «здесь должна быть программа, но вместо этого просто ошибка». error _ :: WriterT w m a
w
Образно говоря, action
псевдокод, который вы опубликовали, внезапно завершает работу program, хотя в типе не упоминалось, что вашей программе было разрешено внезапно завершиться, и вы должны (образно) поблагодарить свои счастливые звезды за то, что вам разрешено настроить программу замены (with try
), а не быть должным образом наказанным заошибка!
С проповедью на вершине башни из слоновой кости давайте предположим, что у нас действительно есть
action :: MonadWriter String m => m ()
action = do tell "a"
tell "b"
error "c"
tell "d"
и нам просто нужно с этим справиться. Предполагая, что вы используете отложенную версию Writer
, вы будете рады отметить, что
runWriter action =
( ()
, "a" "b" (case error "c" of (_, c) -> c) "d"
)
Существует эта функция, которая «спасает» список, перехватывая нечистое исключение (аморальное, «буквально нет программы», о котором я говорил error
), если оно возникает при оценке позвоночника.
-- can be recast as Free (a,) () -> IO (Free (a,) (Maybe e))
-- essentially, that type encodes the intuition that a list may end in [] (nil)
-- or in an error
salvageList :: Exception e => [a] -> IO ([a], Maybe e)
salvageList xs = catch (do xs' <- evaluate xs
case xs' of
[] -> return ([], Nothing)
(x : tl) -> do (tl', e) <- salvageList tl
return (x : tl', e)
) (e -> return ([], Just e))
Что работает:
-- we get the return value, too! that makes me feel... surprisingly weirded out!
yourOutputPlus :: IO ((), Maybe String, String)
yourOutputPlus = do let (val, log) = runWriter action
(realLog, error) <- salvageList log
return (val, fmap ((ErrorCall msg) -> msg) error, realLog)
Ответ №2:
Если вы хотите, чтобы состояние выдерживало подобное исключение во время выполнения, лучше всего использовать изменяемые переменные. Это подход, который мы используем, например, внутри Yesod. В rio
библиотеке есть MonadWriter
экземпляр, основанный на изменяемых ссылках` который работает таким образом:
#!/usr/bin/env stack
-- stack --resolver lts-13.17 script
{-# LANGUAGE NoImplicitPrelude #-}
import Test.Hspec
import RIO
import RIO.Writer
main = hspec $ it "writer and exceptions" $ do
ref <- newSomeRef ""
result <- tryAny $ runRIO ref $ do
tell "a"
tell "b"
error "c"
tell "d"
case result of
Left _ -> pure ()
Right () -> error "it should have failed!!!"
written <- readSomeRef ref
written `shouldBe` "ab"
Я затрону это (и связанные с этим моменты) в своем докладе «Все, что вы не хотели знать о состоянии монадного трансформатора»: