收集错误(而不是短路)直到实际使用值为止

问题描述 投票:0回答:1

在使用错误值之前不短路而是收集错误的最简单但最优雅的方法是什么?

累积错误有什么难的?仅当函数调用接收到错误值作为值时才短路。但随后返回自此以来累积的所有错误。

见解:

  • Monad 会在任何错误时短路,因为
    >>=
    依赖于有一个参数来应用该函数。
  • Applicative
    <*>
    可以从它的两个参数中收集错误。

由于缺少 Monad 实例,代码无法编译(请参阅下面的编译错误),因为我不知道需要如何设计它。该代码旨在显示 Monad 实例或任何其他实现应提供的所需行为。

这是对特定代码解决方案的请求(无论是 Monad 实例还是完全不同的方法),并且 NOT 是对包推荐的请求。

以下语言扩展和包中使用的方法仍然可能会带来一些启发(#haskell IRC 中出现的部分):

以下代码的灵感来自于: 如何收集 Either Monad 中的所有错误消息? https://blog.ploeh.dk/2018/11/05/applicative-validation/

{-# LANGUAGE DeriveFunctor, RecordWildCards, OverloadedStrings #-}

import Data.Text (Text)

newtype Errors e r = Errors (Either e r) deriving (Show, Eq, Functor)

instance Semigroup m => Applicative (Errors m) where
  pure = Errors . pure
  Errors (Left x) <*> Errors (Left y) = Errors (Left (x <> y))
  Errors f <*> Errors r = Errors (f <*> r)

data Result = Result {r1 :: !Int, rg :: !Int} deriving (Show)

f1 :: Errors [Text] Int
f1 = Errors $ Left ["f1 failed"]

f2 :: Errors [Text] Int
f2 = pure 2

f3 :: Errors [Text] Int
f3 = Errors $ Left ["f3 failed"]

f4 :: Errors [Text] Int
f4 = pure 4

f5 :: Errors [Text] Int
f5 = pure 5

g :: Int -> Int -> Errors [Text] Int
g a b | a + b <= 6 = Errors $ Left ["g: a + b NOT > 6"] -- we'll let `g` fail if sum is less than 6
      | otherwise = pure $ a * b

-- | in `scenario1` `g` uses one erroneous and one non-erroneous result.
--   since `g` tries to consume one erroneous result `r3` `g` can't execute.
--   it short-circuits the computation.
--   all up till then collected errors are returned.
--
-- >>> scenario1
-- Errors (Left ["f1 failed", "f3 failed"])
scenario1 :: Errors [Text] Result
scenario1 = do
  r1 <- f1 :: Errors [Text] Int -- fails, collected
  r2 <- f2 :: Errors [Text] Int -- succeeds
  r3 <- f3 :: Errors [Text] Int -- fails, collected
  -- we haven’t short-circuited until here, instead collected errors
  -- although `f1` failed, `f2` and `f3` still have been executed
  -- but now we need to short circuit on `f4` because at least any of `r2` or `r3` has error value
  rg <- g r2 r3 :: Errors [Text] Int
  pure $ Result {..}

-- | `g` can execute (all values are non-errors) but `g` itself produces an error.
--   computation short-circuits only on construction of `Result`.
--   that is because `Result` only carries non-error values but `g` produced error value.
--   `scenario2` returns error values of `f1` and `g`.
--
-- >>> scenario2
-- Errors (Left ["f1 failed", "g: a + b NOT > 6"])
scenario2:: Errors [Text] Result
scenario2 = do
  r1 <- f1 :: Errors [Text] Int -- fails, collected
  r2 <- f2 :: Errors [Text] Int -- succeeds
  r4 <- f4 :: Errors [Text] Int -- succeeds
  -- we haven’t short-circuited until here, instead collected errors
  -- although `f1` failed, `f2` and `f4` still have been executed
  -- `g` receives non-error values `r2` and `r4` with values 2 and 4
  -- now, g itself returns an error due to its logic
  rg <- g r2 r4 :: Errors [Text] Int
  -- we still don’t short-circuit `g`'s error being produced
  -- we only short-circuit on the error value tried being used by `Result`:
  pure $ Result {..}

-- | `g` does neither is fed with erroneous values nor
--   does `g` itself return an error. Instead construction of `Result` fails
--   since it tries to load value of `r1` which is erroneous but should be `Int`.
--
-- >>> scenario3
-- Errors (Left ["f1 failed"])
scenario3 :: Errors [Text] Result
scenario3 = do
  r1 <- f1 :: Errors [Text] Int -- fails, collected
  r2 <- f2 :: Errors [Text] Int -- succeeds
  r5 <- f5 :: Errors [Text] Int -- succeeds
  -- we haven’t short-circuited until here, instead collected errors
  -- although `f1` failed, `f2` and `f4` still have been executed
  -- `g` receives non-error values `r2` and `r5` with values 2 and 5
  -- now, `g` itself succeeds, no error
  rg <- g r2 r5 :: Errors [Text] Int
  -- `Result` is constructed, since `f1` failed, `r1` is of error value now
  -- hence `Result` cannot be constructed, failure "f1 failed" should be returned
  pure $ Result {..}

-- | no error anywhere, 
--
-- >>> scenario4
-- Errors (Right 7)
scenario4 :: Errors [Text] Result
scenario4 = do
  r1 <- f4 :: Errors [Text] Int -- succeeds
  r2 <- f2 :: Errors [Text] Int -- succeeds
  r5 <- f5 :: Errors [Text] Int -- succeeds
  -- now, `g` itself succeeds, no error
  rg <- g r2 r5 :: Errors [Text] Int
  -- `Result` is constructed successfully because it only takes in non-error values
  pure $ Result {..}

错误来了:

rc/MyLib2.hs:42:3: error: [GHC-39999]
    • No instance for ‘Monad (Errors [Text])’
        arising from a do statement
    • In a stmt of a 'do' block: r1 <- f1 :: Errors [Text] Int
      In the expression:
        do r1 <- f1 :: Errors [Text] Int
           r2 <- f2 :: Errors [Text] Int
           r3 <- f3 :: Errors [Text] Int
           rg <- g r2 r3 :: Errors [Text] Int
           ....
      In an equation for ‘scenario1’:
          scenario1
            = do r1 <- f1 :: Errors [Text] Int
                 r2 <- f2 :: Errors [Text] Int
                 r3 <- f3 :: Errors [Text] Int
                 ....
   |
42 |   r1 <- f1 :: Errors [Text] Int -- fails, collected
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, no modules loaded.
haskell error-handling monads applicative semigroup
1个回答
0
投票

到目前为止,我在这种情况下所做的事情并不是特别复杂。 Haskell 可能有一种更优雅的方法来做到这一点,但另一方面,这里接下来的内容可以移植到其他语言。

我解决这类问题的方法是交替使用同一数据的两种表示形式;一种具有类似验证的行为,另一种是适当的 monad。

对于 monad 来说,显然使用内置的

Either
。对于验证表示,我将使用 OP 中提供的
Errors

这两种表示是同构的,但是使用显式函数来回切换可能会很方便:

toEither :: Errors l r -> Either l r
toEither = coerce

fromEither :: Either l r -> Errors l r
fromEither = coerce

此外,我还将利用此扩展:

{-# LANGUAGE ApplicativeDo #-}

通过这些添加,我们可以实现 OP 中的每个场景。

场景1

大多数这些场景都有类似的代码,所以我将主要评论第一个:

scenario1 :: Errors [Text] Result
scenario1 = do
  r1 <- f1
  rg <- fromEither $ do
        r2 <- toEither f2
        r3 <- toEither f3
        toEither $ g r2 r3
  pure $ Result r1 rg

外部

do
表达式使用
Errors
,这就是我们需要
ApplicativeDo
扩展的原因。虽然
Errors
不是
Monad
实例,但该扩展仍然启用
do
语法。

为了将

r2
r3
g
组合在一起,您需要某种
join
功能,因此内部
do
通过转换值在正常
Either
-monad 模式下工作,然后将结果转换回
Errors

它可能不是最优雅的解决方案,但它有效:

ghci> scenario1
Errors (Left ["f1 failed","f3 failed"])

场景2

这个示例以及后续示例都遵循相同的模板:

scenario2 :: Errors [Text] Result
scenario2 = do
  r1 <- f1
  rg <- fromEither $ do
        r2 <- toEither f2
        r4 <- toEither f4
        toEither $ g r2 r4
  pure $ Result r1 rg

演示:

ghci> scenario2
Errors (Left ["f1 failed","g: a + b NOT > 6"])

场景3

代码:

scenario3 :: Errors [Text] Result
scenario3 = do
  r1 <- f1
  rg <- fromEither $ do
        r2 <- toEither f2
        r5 <- toEither f5
        toEither $ g r2 r5
  pure $ Result r1 rg

演示:

ghci> scenario3
Errors (Left ["f1 failed"])

场景4

代码:

scenario4 :: Errors [Text] Result
scenario4 = do
  r1 <- f4
  rg <- fromEither $ do
        r2 <- toEither f2
        r5 <- toEither f5
        toEither $ g r2 r5
  pure $ Result r1 rg

演示:

ghci> scenario4
Errors (Right (Result {r1 = 4, rg = 10}))
© www.soinside.com 2019 - 2024. All rights reserved.