Можно ли получить доступ к частично собранным сообщениям `WriterT` в случае исключения?

#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"
  

Я затрону это (и связанные с этим моменты) в своем докладе «Все, что вы не хотели знать о состоянии монадного трансформатора»: