也许 monad 和一个列表

Maybe monad and a list

好的,所以我正在尝试学习如何使用 monad,从 maybe 开始。我想出了一个例子,但我不知道如何以一种很好的方式应用它,所以我希望其他人可以:

我有一个包含一堆值的列表。根据这些值,我的函数应该 return 列表本身,或者什么都没有。换句话说,我想做一种过滤,但命中的结果是函数失败。

我能想到的唯一方法是使用过滤器,然后比较我返回的列表的大小为零。有没有更好的方法?

这看起来很适合 traverse:

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)

这有点啰嗦,所以让我们针对您的用例进行专门化,使用列表和 Maybe:

GHCi> :set -XTypeApplications
GHCi> :t traverse @[] @Maybe
traverse @[] @Maybe :: (a -> Maybe b) -> [a] -> Maybe [b]

它是这样工作的:你给它一个 a -> Maybe b 函数,它被应用到列表的所有元素,就像 fmap 一样。不同之处在于,Maybe b 值随后以一种仅在没有任何 Nothing 时才为您提供修改后的列表的方式组合;否则,总的结果是Nothing。像手套一样符合您的要求:

noneOrNothing :: (a -> Bool) -> [a] -> Maybe [a]
noneOrNothing p = traverse (\x -> if p x then Nothing else Just x)

allOrNothing 本来是一个更悦耳的名字,但我不得不根据你的描述翻转测试。)

关于 TraversableApplicative 类,我们可能会讨论很多事情。现在,我将多谈谈 Applicative,以防你还没有遇到它。 ApplicativeMonad 的超类,具有两个基本方法:pure,与 return 相同,以及 (<*>),与 (>>=) 但与之截然不同。对于 Maybe 示例...

GHCi> :t (>>=) @Maybe
(>>=) @Maybe :: Maybe a -> (a -> Maybe b) -> Maybe b
GHCi> :t (<*>) @Maybe
(<*>) @Maybe :: Maybe (a -> b) -> Maybe a -> Maybe b

...我们可以这样描述差异:在mx >>= f中,如果mx是一个Just值,(>>=)到达它的内部应用f 并产生一个结果,这取决于 mx 中的内容,结果将是 Just 值或 Nothing。但是,在 mf <*> mx 中,如果 mfmxJust 值,您肯定会得到一个 Just 值,它将保存应用从 mfmx 的值的函数。 (顺便说一句:如果mfmxNothing会怎样?)

traverse 涉及 Applicative 因为我在开头提到的值的组合(在您的示例中,将多个 Maybe a 值转换为 Maybe [a] ) 使用 (<*>) 完成。由于您的问题最初是关于 monad 的,因此值得注意的是可以使用 Monad 而不是 Applicative 来定义 traverse。此变体的名称为 mapM:

mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)

我们更喜欢 traverse 而不是 mapM 因为它更通用——如上所述,ApplicativeMonad 的超类。

最后,您对这个 "a sort of filter" 的直觉很有道理。特别是,考虑 Maybe a 的一种方法是,当您选择布尔值并将类型 a 的值附加到 True 时,它就是您得到的结果。从那个有利的角度来看,(<*>) 作为这些奇怪的布尔值的 &&,如果您碰巧提供其中的两个值,它会结合附加值(参见 of an implementation using any). Once you get used to Traversable, you might enjoy having a look at the Filterable and Witherable classes,这与这种关系有关在 MaybeBool 之间。

duplode 的回答很好,但我认为学习以更基本的方式在 monad 中操作也很有帮助。学习每个小的 monad-general 函数,并了解它们如何组合在一起解决特定问题可能是一个挑战。因此,这里有一个 DIY 解决方案,展示了如何使用 do 符号和递归,这些工具可以帮助您解决任何一元问题。

forbid :: (a -> Bool) -> [a] -> Maybe [a]
forbid _ [] = Just []
forbid p (x:xs) = if p x
  then Nothing
  else do
    remainder <- forbid p xs
    Just (x : remainder)

将此与 remove 的实现进行比较,filter 的相反实现:

remove :: (a -> Bool) -> [a] -> [a]
remove _ [] = []
remove p (x:xs) = if p x
  then remove p xs
  else
    let remainder = remove p xs
    in x : remainder

结构是一样的,只有几个区别:当谓词 return 为真时你想做什么,以及你如何访问递归 returned 的值称呼。对于 remove,returned 值是一个列表,因此您可以 let-绑定它并对其进行约束。使用 forbid,returned 值只是 maybe 一个列表,因此您需要使用 <- 绑定到该 monadic 值。如果 return 值为 Nothing,bind 将使计算短路并且 return Nothing;如果它只是一个列表,do 块将继续,并将一个值放在该列表的前面。然后你将它包装在一个 Just 中。