将 monadic 验证变成应用程序?

Turn monadic validation into applicative?

我需要验证数字输入是否在特定范围内。为此,我正在使用

ensure :: (a -> Bool) -> a -> Maybe a
ensure p v | p v       = Just v
           | otherwise = Nothing

检查某个值的上限和下限的一种方法 x :: Int 是通过单子链:

let validated = pure x >>= ensure (>0) >>= ensure (<100)

据我了解,两次验证的顺序无关紧要;因此,应该有可能以应用形式重写上述表达式。如何? 我没能做到,但我希望一旦我做到了,就能对应用程序有更深入的了解:-)。

(这是这个答案的全新版本)。

你有

ensure :: (a -> Bool) -> a -> Maybe a
ensure p v | p v       = Just v
           | otherwise = Nothing

实际上是

ensure :: Alternative m => (a -> Bool) -> a -> m a
ensure p v = if p v then pure v else empty

所以

validated x  =  pure x >>= ensure (>0) >>= ensure (<100)
  = do
       a <- pure x
       b <- ensure (>0) a
       c <- ensure (<100) b
       pure c
  = do
       b <- ensure (>0) x
       c <- ensure (<100) b
       pure c

如果 ensure 是一个不透明函数,我们将不得不到此为止。

但是因为我们知道它的来源,所以我们可以继续,

  = do
       b <- if (>0) x then pure x else empty
       c <- if (<100) b then pure b else empty
       pure c
  = do
       _ <- if (>0) x then pure () else empty
       _ <- if (<100) x then pure () else empty
       pure x
  = do
       if (>0) x then pure () else empty
       if (<100) x then pure () else empty
       pure x

  = guard ((>0) x) >> guard ((<100) x) >> pure x

  = [ x | x > 0, x < 100 ]   -- with MonadComprehensions

这个计算本质上没有什么一元的,没有在上一步计算的值在这里计算下一个计算步骤,只是从给定值计算的一个计算步骤,x:

  = guard ((>0) x) *> guard ((<100) x) *> pure x

  = liftA2 (\_ _-> x) (guard $ (>0) x) (guard $ (<100) x)

  = when (x>0) (when (x<100) $ pure ()) *> pure x

  = when (and [x>0, x<100]) (pure ()) *> pure x

最后一个表达式的推断类型是Applicative f => f b, x :: b

这里要注意的非常微妙的一点是,“直觉上”ensure 的 return 值并不重要。重要的只是它是 Just 还是 Nothing。为了表达这一点,您可以给它一个更诚实的类型签名:

ensure :: (a -> Bool) -> a -> Maybe ()
ensure p v | p v       = Just ()
           | otherwise = Nothing

现在,由于 return 值无关紧要,您应该安全地忽略它。所以逻辑变成了这样:调用 ensure (>0)ensure (<100),然后将它们组合在一起,忽略它们的 return 值,但保留它们的 Maybe 形状。

为此,您确实可以使用 applicative。您要应用的函数将执行我上面所说的:忽略两个 return 值。 “保留 Maybe 形状”部分将由 Applicative 实例透明处理。所以:

let validated = (\_ _ -> x) <$> ensure (>0) x <*> ensure (<100) x

看看我应用的函数如何忽略参数和 returns x 本身?


但是,当然,ensure 的类型签名,如您在问题中所写,表明它可能做的不仅仅是验证。当然,当它完全通用时它确实不能,但如果它更具体一点就可以:

ensure :: (Int -> Bool) -> Int -> Maybe Int
ensure p v | p v       = Just (v + 100)
           | otherwise = Nothing

现在顺序突然很重要了:

pure (-5) >>= ensure (>0) >>= ensure (<100) == Nothing
pure (-5) >>= ensure (<100) >>= ensure (>0) == Just 95

所以这里的底线是:ensure 的目的有点模棱两可。真的是returnMaybe a吗?那么顺序可能很重要。真正的return类型是Maybe ()吗?然后你可以使用 Applicative 并忽略那些单位。

您可以使用 liftA2 来组合两个谓词:

> import Control.Applicative
> :t liftA2 (&&) (> 0) (< 100)
liftA2 (&&) (> 0) (< 100) :: (Ord a, Num a) => a -> Bool

此函数具有与 ensure 一起使用的正确类型:

validate :: Num a => a -> Bool
validate f g = ensure (liftA2 (&&) f g)

然后

> validate (> 0) (< 100) 50
Just 50
> validate (> 0) (< 100) 1000
Nothing

没有。 Applicative 和 Monad 之间的区别虽然都在应用程序之间强制执行顺序,但 Monad 可以接受链中其他操作的值,而 Applicative 不能。 Maybe 有一个 Applicative 实例,但它的工作方式就像 Maybe Monoid:returns 最左边的 Just 值。换句话说,Applicative 实例适用于 Maybe a 的值,但不适用于 a -> Maybe a.