【问题标题】:Combining validators in applicative style in Haskell在 Haskell 中以应用风格组合验证器
【发布时间】:2017-05-05 09:35:02
【问题描述】:

我对命令式编程有很好的掌握,但现在我自学了一个 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

现在我想要由evenisSmall 构建的validate :: Integer -&gt; Maybe Integer

我最好的解决方案是

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

这不是无意义的

我可以使用单子

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) -&gt; Maybe (Integer, Integer),它在输入元组上尝试greater,然后在元组的第二个元素上尝试even

validate' :: (Integer, Integer) -&gt; 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 会做什么?

【问题讨论】:

  • 为什么所有这些都返回Just input成功?更简单的True 不会提供相同的信息吗?
  • 那还不是Applicative,我想学习如何使用Applicatives
  • Applicative 在这里有点滥用。您真正需要的唯一“抽象”是MonoidApplicative 在这种情况下和Monad 一样没用,呵呵。如果您想了解 Applicatives,那么寻找没有价值的人为用例可能不是一条有效的途径:)
  • 如果您尝试的转换可能会失败,这可能更有意义,例如 String -> Maybe Int。
  • 看起来你有两个非常简单的函数:validate k x = k x *&gt; pure xbool2A x = if x then pure () else empty - 然后你只有 validate (bool2A . Prelude.even)validate (bool2A . (&lt;10)) - 应该很明显如何组合函数类型 a -&gt; Bool以你想要的方式(提示:它的liftA2 (&amp;&amp;) - 和liftA2 (||) 得到“两个条件都成立”的对偶,即“任何一个条件成立”) - 所以你的第一个例子非常简单validate (bool2A . liftA2 (&amp;&amp;) (&lt;10) Prelude.even)

标签: haskell composition applicative


【解决方案1】:

当您编写a -&gt; Maybe b 形式的验证器时,您对整个类型比对Maybe 应用程序更感兴趣。 a -&gt; 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

它们以不同的顺序检查条件。如果您关心评估顺序,您可以定义另一个函数&lt;*&lt;

阅读器T

如果您经常使用 a -&gt; Maybe b 类型,则可能值得为它创建一个 newtype,以便您可以添加自己的实例来执行您希望它执行的操作。 newtype 已经存在;它是ReaderT,它的实例已经做了你想做的事情。

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

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

instance (Applicative m) => Applicative (ReaderT r m) where
    f <*> v = ReaderT $ \ r -> runReaderT f r <*> runReaderT v r
    ...

ReaderT&lt;*&gt; 与第一部分中的&gt;*&gt; 几乎完全相同,但它不会丢弃第一个结果。 ReaderT*&gt; 与第一部分中的 &gt;*&gt; 完全相同。

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

您对 x 的值 runReaderT validate x 使用这些 ReaderT 验证器之一

【讨论】:

    【解决方案2】:

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

    您所做的一切本质上是在尝试利用 Monoidal 行为/Monoid,但尝试通过 Applicative 接口来实现。有点像使用Int's 通过他们的字符串表示(实现+ 为字符串"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 视为将Monoid 实例“提升”为免费的Applicative/Monad 实例。

    最后,您会注意到一个有用的设计模式/抽象,但您真正注意到的是 monoid。您正专注于通过 Applicative 接口处理该 monoid,不知何故……但直接使用 monoid 可能更简单。

    还有,

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

    可以说在清晰度上与带有 Writer 的 do 表示法版本相当:)

    【讨论】:

    • 是的,这很牵强。
    • 公平地说,“通过 Writer/Applicative/Monad 使用 Monoid”模式在 Haskell 生态系统中很常见
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-18
    • 2010-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多