在使用错误值之前不短路而是收集错误的最简单但最优雅的方法是什么?
累积错误有什么难的?仅当函数调用接收到错误值作为值时才短路。但随后返回自此以来累积的所有错误。
见解:
>>=
依赖于有一个参数来应用该函数。<*>
可以从它的两个参数中收集错误。由于缺少 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 可能有一种更优雅的方法来做到这一点,但另一方面,这里接下来的内容可以移植到其他语言。
我解决这类问题的方法是交替使用同一数据的两种表示形式;一种具有类似验证的行为,另一种是适当的 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 中的每个场景。
大多数这些场景都有类似的代码,所以我将主要评论第一个:
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"])
这个示例以及后续示例都遵循相同的模板:
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"])
代码:
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"])
代码:
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}))