在 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 Integer
从 even
和 isSmall
构建
我最好的解决方案是
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"进入 snd
和 even
的某种组合,然后只有单个元素最终出现在最终的 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"
实现 +
并处理字符串而不仅仅是 1
和12
并使用整数)
请注意,您可以从任何 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
实例(通过 Writer
和 tell
),这使它成为一个有点方便。您可以将此处的 Writer
/tell
视为 "lifting" 一个 Monoid
实例到一个免费的 Applicative
/Monad
实例。
最后,您会注意到一个有用的设计 pattern/abstraction,但您真正注意到的是 monoid。您专注于通过 Applicative 接口处理那个幺半群,不知何故……但直接使用幺半群可能更简单。
此外,
validate :: Int -> All
validate = mconcat
[ isSmall
, even
, other validator
]
可以说在清晰度上与带 Writer 的 do 符号版本相当 :)
我对命令式编程有很好的掌握,但现在我自己学习了 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 Integer
从 even
和 isSmall
我最好的解决方案是
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"进入 snd
和 even
的某种组合,然后只有单个元素最终出现在最终的 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"
实现 +
并处理字符串而不仅仅是 1
和12
并使用整数)
请注意,您可以从任何 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
实例(通过 Writer
和 tell
),这使它成为一个有点方便。您可以将此处的 Writer
/tell
视为 "lifting" 一个 Monoid
实例到一个免费的 Applicative
/Monad
实例。
最后,您会注意到一个有用的设计 pattern/abstraction,但您真正注意到的是 monoid。您专注于通过 Applicative 接口处理那个幺半群,不知何故……但直接使用幺半群可能更简单。
此外,
validate :: Int -> All
validate = mconcat
[ isSmall
, even
, other validator
]
可以说在清晰度上与带 Writer 的 do 符号版本相当 :)