我对命令式编程有很好的把握,但是现在我学会了很好的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
但是为什么要使用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
但是我想输入元组&#34;流动&#34; 到greater
,然后&#34;流动&#34; 到一些snd
和even
的一种组合,然后只有一个元素在最终的Just
中结束。
哈斯克勒会做什么?
答案 0 :(得分:7)
当您编写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
这些以不同的顺序检查条件。如果您关心评估顺序,则可以定义另一个函数<*<
。
如果您经常使用类型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
&#39; <*>
与第一部分的>*>
几乎完全相同,但它不会丢弃第一个结果。 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
您可以ReaderT
使用其中一个x
验证码runReaderT validate x
答案 1 :(得分:5)
如果你需要的是申请人,你问为什么要使用Monad?我可以问 - 为什么使用Applicative,如果您只需要Monoid?
你所做的一切主要是试图利用monoid行为/ Monoid,但试图通过Applicative接口来实现。有点像通过字符串表示使用Int
&#{为字符串+
和"1"
实施"12"
并使用字符串而不仅仅是{{1} }和1
并使用ints)
请注意,您可以从任何12
实例获取Applicative
实例,因此找到可以解决问题的Monoid与找到可以解决问题的应用程序相同。
Monoid
为了证明它们是相同的,我们可以来回编写转换函数:
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
视为&#34;提升&#34; tell
实例到免费的Monoid
/ Applicative
个实例。
最后,您注意到一个有用的设计模式/抽象,但它确实是您注意到的 monoid 。你通过Applicative接口专注于处理那个monoid,不知怎的......但是直接使用monoid可能更简单。
此外,
Monad
可以说明显与编纂版本符号相似:)