在 Haskell 中以应用风格组合验证器

Combining validators in applicative style in Haskell

我对命令式编程有很好的掌握,但现在我自己学习了 Haskell 非常好。

我认为,我对 Monads、Functors 和 Applicatives 有很好的理论理解,但我需要一些练习。为了练习,我有时会从我当前的工作任务中获取一些信息。

而且我对以应用方式组合内容有些困惑

第一个问题

我有两个验证函数:

import Prelude hiding (even)

even :: Integer -> Maybe Integer
even x = if rem x 2 == 0 then Just x else Nothing

isSmall :: Integer -> Maybe Integer
isSmall x = if x < 10 then Just x else Nothing

现在我想要 validate :: Integer -> Maybe IntegerevenisSmall

构建

我最好的解决方案是

validate a = isSmall a *> even a *> Just a

而且不是免费的

我可以使用 monad

validate x = do
  even x
  isSmall x
  return x

但是,如果(我想)我只需要一个 Applicative,为什么还要使用 Monad? (而且它仍然不是免费的)

这样做更好(也更实用)吗?

第二个问题

现在我有两个具有不同签名的验证器:

even = ...

greater :: (Integer, Integer) -> Maybe (Integer, Integer)
-- tuple's second element should be greater than the first
greater (a, b) = if a >= b then Nothing else Just (a, b)

我需要 validate :: (Integer, Integer) -> Maybe (Integer, Integer),它在输入元组上尝试 greater,然后在元组的第二个元素上尝试 even

validate' :: (Integer, Integer) -> Maybe Integer具有相同的逻辑,但返回元组的第二个元素。

validate  (a, b) = greater (a, b) *> even b *> Just (a, b)
validate' (a, b) = greater (a, b) *> even b *> Just  b

但是我想象输入元组"flows"变成greater,然后"flows"进入 sndeven 的某种组合,然后只有单个元素最终出现在最终的 Just.

haskeller 会做什么?

当您编写 a -> Maybe b 形式的验证器时,您对整个类型比 Maybe 应用程序更感兴趣。 a -> Maybe b 类型是 Maybe monad 的 Kleisli 箭头。您可以制作一些工具来帮助处理这种类型。

第一题你可以定义

(>*>) :: Applicative f => (t -> f a) -> (t -> f b) -> t -> f b
(f >*> g) x = f x *> g x

infixr 3 >*>

并写

validate = isSmall >*> even

你的第二个例子是

validate = even . snd >*> greater
validate' = even . snd >*> fmap snd . greater

这些以不同的顺序检查条件。如果您关心评估顺序,您可以定义另一个函数 <*<.

读者T

如果您经常使用 a -> Maybe b 类型,可能值得为它创建一个 newtype,这样您就可以添加自己的实例来实现您想要它执行的操作。 newtype 已经存在;它是 ReaderT,它的实例已经完成了您想要做的事情。

newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }

当您使用类型 r -> Maybe a 作为验证器来验证和转换单个输入 r 时,它与 ReaderT r Maybe 相同。 Applicative instance for ReaderT 通过将它们的两个函数应用于相同的输入然后将它们与 <*>:

组合在一起,将它们组合在一起
instance (Applicative m) => Applicative (ReaderT r m) where
    f <*> v = ReaderT $ \ r -> runReaderT f r <*> runReaderT v r
    ...

ReaderT<*> 与第一部分中的 >*> 几乎完全相同,但它不会丢弃第一个结果。 ReaderT*> 与第一节中的 >*> 完全相同。

ReaderT 而言,您的示例变为

import Control.Monad.Trans.ReaderT

checkEven :: ReaderT Integer Maybe Integer
checkEven = ReaderT $ \x -> if rem x 2 == 0 then Just x else Nothing

checkSmall = ReaderT Integer Maybe Integer
checkSmall = ReaderT $ \x -> if x < 10 then Just x else Nothing

validate = checkSmall *> checkEven

checkGreater = ReaderT (Integer, Integer) Maybe (Integer, Integer)
checkGreater = ReaderT $ \(a, b) = if a >= b then Nothing else Just (a, b)

validate = checkGreater <* withReaderT snd checkEven
validate' = snd <$> validate

您使用 runReaderT validate x

对值 x 的其中一个 ReaderT 验证器

你问如果你只需要 Applicative 为什么要使用 Monad?我可以问——如果你只需要 Monoid,为什么要使用 Applicative?

你所做的一切本质上是试图利用幺半群 behavior/a 幺半群,但试图通过应用接口来实现。有点像通过 Int 的字符串表示来处理它们(为字符串 "1""12" 实现 + 并处理字符串而不仅仅是 112 并使用整数)

请注意,您可以从任何 Monoid 实例中获得一个 Applicative 实例,因此找到一个可以解决您的问题的 Monoid 与找到一个可以解决问题的 Applicative 是一样的。

even :: Integer -> All
even x = All (rem x 2 == 0)

isSmall :: Integer -> All
isSmall x = All (x < 10)

greater :: (Integer, Integer) -> All
greater (a, b) = All (b > a)

为了证明它们是一样的,我们可以来回写转换函数:

convertToMaybeFunc :: (a -> All) -> (a -> Maybe a)
convertToMaybeFunc f x = guard (getAll (f x)) $> x

-- assuming the resulting Just contains no new information
convertFromMaybeFunc :: (a -> Maybe b) -> (a -> All)
convertFromMaybeFunc f x = maybe (All False) (\_ -> All True) (f x)

你可以直接写你的 validate:

validate :: Int -> All
validate a = isSmall a <> even a

但是你也可以写成你想要的提升风格:

validate :: Int -> All
validate = isSmall <> even

想要做符号吗?

validate :: Int -> All
validate = execWriter $ do
    tell isSmall
    tell even
    tell (other validator)

validate' :: (Int, Int) -> All
validate' = execWriter $ do
    tell (isSmall . fst)
    tell (isSmall . snd)
    tell greater

如您所见,每个 Monoid 实例都会产生一个 Applicative/Monad 实例(通过 Writertell),这使它成为一个有点方便。您可以将此处的 Writer/tell 视为 "lifting" 一个 Monoid 实例到一个免费的 Applicative/Monad 实例。

最后,您会注意到一个有用的设计 pattern/abstraction,但您真正注意到的是 monoid。您专注于通过 Applicative 接口处理那个幺半群,不知何故……但直接使用幺半群可能更简单。

此外,

validate :: Int -> All
validate = mconcat
  [ isSmall
  , even
  , other validator
  ]

可以说在清晰度上与带 Writer 的 do 符号版本相当 :)