如何使用Maybe Monoid并使用自定义操作轻松地组合值?

11

我想要做的事情通过手工定义是微不足道的,基本上

maybeCombine :: (a->a->a) -> Maybe a -> Maybe a -> Maybe a
maybeCombine _ Nothing Nothing = Nothing
maybeCombine _ (Just a) Nothing = Just a
maybeCombine _ Nothing (Just a) = Just a
maybeCombine f (Just a) (Just a') = Just $ f a a'

在需要时定义此局部并不是什么大问题,但仍然很繁琐,而且这似乎是一个基本的通用问题,应该有一个标准的实现,但我似乎找不到。

也许我只是忽略了一些东西。我想要的与maybe monad的行为完全无关,因此我认为在Monad / Arrow抽屉中找不到任何东西; 但它确实类似于Monoid实例

预处理Data.Monoid> Just "a" <> Nothing
Just "a"
预处理Data.Monoid> Just "a" <> Just "b"
Just "ab"
...

…但是它要求a本身是一个monoid,即它基本上具有内置的a->a->aMonadPlus实例的行为也很符合我所需的,但它只是扔掉其中一个值,而不是允许我提供一个组合函数

Prelude Data.Monoid Control.Monad> Just 4 `mplus` Nothing
  Just 4
Prelude Data.Monoid Control.Monad> Nothing `mplus` Just 4
  Just 4
Prelude Data.Monoid Control.Monad> Just 4 `mplus` Just 5
  Just 4
什么是规范解决方案? 本地模式匹配?使用例如Data.Maybe的组合器?定义自定义Monoid来进行组合?
3个回答

12

您始终可以使用

f <$> m <*> n <|> m <|> n

但是,遗憾的是,在任何地方都没有一个规范的实现。

你可以使用reflection来获取将(a -> a -> a)内置作为Semigroup用于与Option一起使用,这由semigroups提供,作为Maybe的改进版本,具有与Semigroup相关的Monoid的“正确”实例。不过,对于这个问题来说,这种方法过于笨重了。=)

也许应该将其添加为组合器到Data.Maybe中。


对,我忘记了Alternative!这个很不错,但我觉得我更喜欢Semigroup的优雅。 - leftaroundabout

11
当你注意到f就像底层a类型上的Monoid操作时,你是正确的。更具体地说,这里正在通过添加零(mempty)Nothing将一个Semigroup提升为Monoid。 这正是MaybeMonoid在Haddocks中所看到的。引用:“将一个Semigroup提升为Maybe形成一个Monoid,根据http://en.wikipedia.org/wiki/Monoid:“任何Semigroup S都可以简单地通过添加一个元素e而不是S,并定义ee = e和es = s = s*e对于所有s ∈ S。”由于没有“Semigroup”类型类只提供mappend,因此我们使用Monoid。”

或者,如果您喜欢semigroups包,那么有一个Option,它具有完全相同的行为,适当地推广到使用底层的Semigroup


因此,这表明最清晰的方法是在底层类型a上定义MonoidSemigroup实例。这是一种干净的方式,将某些组合器f与该类型相关联。

如果您无法控制该类型,不想使用孤立实例,并认为newtype包装很丑怎么办?通常情况下,您会没有办法,但这是一个使用完全黑魔法,有效地仅适用于 GHC 的reflection包非常有用的地方。在论文本身中存在详细的解释in the paper itself, 但Ausin Seipp's FP Complete Tutorial包括一些示例代码,允许您“注入”任意半群乘积到类型中,而不需要(尽可能多的)类型定义噪音...以更可怕的签名为代价。

然而,这可能比它的价值要高得多。


1
我并不是运气不好,在我现在处理的问题中,我实际上可以使用“Max”半群并得到一个非常好的解决方案! - leftaroundabout
3
太棒了!我经常将Option (Max a)定义为在类型上添加"负无穷",所以绝对赞同。我认为这是一个非常优雅的Monoid - J. Abrahamson

2
import Data.Monoid
maybeCombine :: (a->a->a) -> Maybe a -> Maybe a -> Maybe a
maybeCombine f mx my = let combine = mx >>= (\x -> my >>= (\y -> Just (f x y)))
                       in getFirst $ First combine `mappend` First mx `mappend` First` my

在GHCi中,这个命令会给我返回:
*Main> maybeCombine (+) Nothing Nothing
Nothing
*Main> maybeCombine (+) (Just 3) Nothing
Just 3
*Main> maybeCombine (+) (Just 3) (Just 5)
Just 8

如果在 mappend 序列的末尾添加 Last combine,则 getLast 也可以使用。


我承认这不像J. Abrahamson的解决方案那样通用 ;) - itsbruce
+1 对于之前未解决的(也许是最直接的)问题方面的回答...但说实话,这个实现既不比我原来愚蠢的模式匹配更短,也不更易读。我也无法完全看出它如何更好地适应需求的变化。 - leftaroundabout
1
你可以将这个方法压缩成类似于Ed的解决方案的简短形式——getFirst . mconcat . map First $ [liftA2 f mx my, mx, my],甚至可以用msum替换getFirst . mconcat . map First,尽管没有First可能会更不清晰。 - J. Abrahamson
1
@leftaroundabout 如果使用“do”符号会更美观,我知道可以通过提升来缩短代码(感谢J ;)),但我只想让基本技术清晰明了。 - itsbruce
不是这样的...我的意思是,我没有看到任何应用场景,你的建议实际上比之前的任何一个更好。它仍然是一个有用的答案,我认为“First”单子是一个有趣和意外的考虑,可能在其他问题中会派上用场。只是,我接着就想不出任何类似于这个问题的问题,因此我的话可能会让人感到困惑。 - leftaroundabout
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接