将多个 IO 异常提取为多义词

Extracting multiple IO exceptions into polysemy

假设我有一些非常复杂的计算集,形式为 computation :: IO a,我无法修改,因为它来自某些库代码或其他原因。假设我想提供一些类型级别的保证,即我们无法在使用这些计算的过程中发射核导弹,所以我们使用 polysemy 并将所有这些都包装到它自己的 DSL Library.我们可以天真地解释为

runLibraryIO :: Member (Embed IO) r => Sem (Library ': r) a -> Sem r a
runLibraryIO = interpret $ \case
  Computation -> embed computation
  -- ...

一切都很好,但是 computation 可能会抛出异常!我们很快抛出了一些代码,并且能够将 单一 类型的异常提升为 polysemy。我们写 helper

withIOError :: forall e r a . (Exception e, Members '[Embed IO, Error e] r) => IO a -> Sem r a
withIOError action = do
  res <- embed $ try action
  case res of
       Left e  -> throw @e e
       Right x -> pure x

然后我们的解释器变成了

runLibraryIO :: Members '[Embed IO, Error MyException] r => Sem (Library ': r) a -> Sem r a
runLibraryIO = interpret $ withIOError @MyException . \case
  Computation -> computation
  -- ...

但我们很快注意到这是不可扩展的。特别是,我们只能解除一种类型的异常,并且仅限于 IO 中的计算。我们不能任意深入到包含异常的 monad 中,然后以一种良好而纯粹的方式将其传递出去。如果出于某种原因我们发现 computation 可能抛出 MyException' 的极端情况,我们无法插入对此的支持并在我们的代码中的其他地方捕获它!

库中是否缺少允许我执行此操作的内容?我是否坚持处理 IO 中的异常?非常感谢关于从这里向前推进并使其充分多态的一些指导。

我们可以用 lower 解释器解决这个问题。谢谢@KingoftheHomeless。

withException :: forall e r a . (E.Exception e, Member IOError r, Member (Error e) r) => Sem r a -> Sem r a
withException action = catchIO @r @e action throw

lowerIOError :: Member (Embed IO) r => (forall x. Sem r x -> IO x) -> Sem (IOError ': r) a -> Sem r a
lowerIOError lower = interpretH $ \case
  ThrowIO e -> embed $ E.throwIO e
  CatchIO m h -> do
    m' <- lowerIOError lower <$> runT m
    h' <- (lowerIOError lower .) <$> bindT h
    s  <- getInitialStateT
    embed $ lower m' `E.catch` \e -> lower (h' (e <$ s))

请参阅此 gist 以了解实际效果。