选择非空的单体



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

> [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

如果你的列表最多包含一个元素,它们与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类型为代价的。

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

在我的评论中,当我说"幺半事物没有内省的概念"时——我的意思是你不能对幺半群进行分析(模式匹配、相等、<、>等)。这当然是显而易见的 - Monoids 的 API 只是单位(mempty) 和一个操作mappend(更抽象地说是 <>),它接受两个单表事物并返回一个。类型的mappend定义可以自由使用案例分析,但之后你可以对幺半的东西做的就是使用 Monoid API。

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

首先,你的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的使用应该谨慎看待,因为正如我相信许多哈斯克勒抱怨的那样,这是一个没有原则的阶级。

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

import Data.Monoid
morMaybe :: Maybe a -> Maybe a -> Maybe a
morMaybe x y = getFirst (First x <> First y)

因此,我们可以将其与列表一起使用,就像在您的示例中一样,在[a]Maybe (NonEmpty a)之间的(nonEmpty, maybe [] toList)同构下:

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

不是每个幺半群都可以有TestableMonoid的实例,但很多(包括列表)都可以。

instance TestableMonoid [a]
where
isMempty = null

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

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 xiffmappend x = id

但我确实质疑这是否真的有任何常见的用例。正如我之前所说,Monoid 约束对于您的用例来说是多余的,因为您永远不会mappend.因此,我们应该问,我们是否可以设想一种情况,在这种情况下,一个人可能需要mappend又需要isMempty,因此对TestableMonoid约束有合理的需要?我在这里可能目光短浅,但我无法想象一个案例。

我认为这是因为斯蒂芬·泰特利(Stephen Tetley)在说这"违背其Monoid的精神"时谈到了某些事情。将头倾斜在mappend的类型签名上,括号略有不同:

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

mappend是从集合的成员a到函数a -> a的映射。幺半群是一种将值视为这些值之上的函数的方法。幺半群是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]

最新更新