选择非空Monoid

时间:2012-11-28 22:22:36

标签: haskell monoids

我需要一个能选择非空幺半群的函数。对于列表,这将意味着以下行为:

> [1] `mor` []
[1]
> [1] `mor` [2]
[1]
> [] `mor` [2]
[2]

现在,我实际上已经实现了它,但我想知道是否存在一些标准替代方案,因为它似乎是一种常见的情况。不幸的是,Hoogle没有帮助。

这是我的实施:

mor :: (Eq a, Monoid a) => a -> a -> a
mor a b = if a /= mempty then a else b

3 个答案:

答案 0 :(得分:3)

如果您的列表最多只包含一个元素,那么它们与Maybe同构,为此,First中存在“第一个非空”幺半群:Data.Monoid。它是Maybe a值的包装,mappend返回第一个Just值:

import Data.Monoid
main = do
    print $ (First $ Just 'a') <> (First $ Just 'b')
    print $ (First $ Just 'a') <> (First Nothing)
    print $ (First Nothing)    <> (First $ Just 'b')
    print $ (First Nothing)    <> (First Nothing :: First Char)

==> Output:
First {getFirst = Just 'a'}
First {getFirst = Just 'a'}
First {getFirst = Just 'b'}
First {getFirst = Nothing}

使用[a] -> Maybe a即可实现转化Data.Maybe.listToMaybe

旁注:这个不限制包装类型的类型类;在您的问题中,您需要一个Eq实例来比较与mempty的相等性。这当然是以Maybe类型为代价的。

答案 1 :(得分:2)

[这真是一个长期评论而不是答案]

在我的评论中,当我说“幺正的东西没有内省的概念”时 - 我的意思是你不能对幺半群进行分析(模式匹配,平等,&lt;,&gt;等)。这当然是显而易见的 - Monoids的API只有单位(mempty)和一个操作 mappend (更抽象地&lt;&gt;),它需要两个单一的东西并返回一个。一个类型的 mappend 的定义可以免费使用案例分析,但是之后你可以使用Monoid API进行所有操作。

Haskell社区中的一种民间传说是避免发明东西,而宁愿使用来自数学和计算机科学的对象(包括函数式编程历史)。结合Eq(需要分析是参数)和Monoid引入了一类新的东西 - 幺半群支持足够的内省以允许平等;并且在这一点上有一个合理的论点,即Eq-Monoid的东西违背了它的Monoid超类的精神(Monoids是不透明的)。由于这既是一类新的对象,也可能是有争议的 - 标准实现将不存在。

答案 2 :(得分:0)

首先,您的mor函数看起来很可疑,因为它需要Monoid但从不使用mappend,因此它比必要的约束更多。

mor :: (Eq a, Monoid a) => a -> a -> a
mor a b = if a /= mempty then a else b

只用Default约束就可以完成同样的事情:

import Data.Default

mor :: (Eq a, Default a) => a -> a -> a
mor a b = if a /= def then a else b

并且我认为Default的任何使用 也应该谨慎查看,因为我相信许多Haskellers抱怨,这是一个没有原则的课程。

我的第二个想法是,您真正处理的数据类型似乎是Maybe (NonEmpty a),而不是[a]Monoid你实际上谈论的是First

import Data.Monoid

morMaybe :: Maybe a -> Maybe a -> Maybe a
morMaybe x y = getFirst (First x <> First y)

然后我们就可以在列表中使用它,就像在您的示例中一样,在(nonEmpty, maybe [] toList)[a]之间的Maybe (NonEmpty a)同构下:

import Data.List.NonEmpty

morList :: [t] -> [t] -> [t]
morList x y = maybe [] toList (nonEmpty x `mor` nonEmpty y)
λ> mor'list [1] []
[1]

λ> mor'list [] [2]
[2]

λ> mor'list [1] [2]
[1]

(我确信更熟悉镜头库的人可以在这里提供更令人印象深刻的简洁演示,但我不会立即知道如何。)

可以使用谓词扩展Monoid来测试元素是否是一个标识。

class Monoid a => TestableMonoid a
  where
    isMempty :: a -> Bool

    morTestable :: a -> a -> a
    morTestable x y = if isMempty x then y else x

并非每个monoid都有TestableMonoid的实例,但是很多(包括list)可以。

instance TestableMonoid [a]
  where
    isMempty = null

我们甚至可以用Monoid编写一个newtype包装器:

newtype Mor a = Mor { unMor :: a }

instance TestableMonoid a => Monoid (Mor a)
  where
    mempty = Mor mempty
    Mor x `mappend` Mor y = Mor (morTestable x y)
λ> unMor (Mor [1] <> Mor [])
[1]

λ> unMor (Mor [] <> Mor [2])
[2]

λ> unMor (Mor [1] <> Mor [2])
[1]

这样就留下了TestableMonoid类是否值得存在的问题。至少,它似乎比Default更像一个“代数合法”的类,因为我们可以给它一个与Monoid相关的法则:

  • isEmpty x iff mappend x = id

但我确实怀疑这是否确实存在任何常见用例。正如我之前所说,Monoid约束对于您的用例来说是多余的,因为您永远不会mappend。那么我们应该问一下,我们是否可以设想一个人可能需要 mappendisMempty的情况,因此合法需要TestableMonoid约束?我可能在这里短视,但我无法想象一个案例。

我认为这是因为斯蒂芬泰特在谈到这种“违背其幺半群的精神”时所触及的东西。将您的头部倾向于mappend的类型签名,并使用略微不同的括号:

mappend :: a -> (a -> a)

mappend是从集合a的成员到函数a -> a的映射。 monoid是一种将视为函数的方式。幺半群是通过这些函数让我们看到的窗口的a 世界的视图。他们让我们看到的功能非常有限。我们唯一能做的就是应用它们。我们从不问任何其他函数(正如斯蒂芬所说,我们没有内省)。因此,虽然,是的,你可以将你想要的任何东西都绑定到一个子类上,在这种情况下,我们正在使用的东西与我们扩展的基类在性质上有很大不同,并且感觉不太可能存在很多交集。使用函数的例子和具有一般相等性的事物的用例或像isMempty这样的谓词。

所以最后我想回到简单而精确的方式来编写这个:在值级别编写代码并停止担心类。您不需要Monoid而且您不需要Eq,您只需要一个额外的参数:

morSimple :: (t -> Bool) -- ^ Determine whether a value should be discarded
          -> t -> t -> t
morSimple f x y = if f x then y else x
λ> morSimple null [1] []
[1]

λ> morSimple null [1] [2]
[1]

λ> morSimple null [] [2]
[2]