我需要一个选择非空幺半群的函数。对于列表,这将意味着以下行为:
> [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
from 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 只是一个单元(空的)和一个操作mappend(更抽象地说是 <>),它接受两个单调的东西并返回一个。为一个类型定义mappend是免费的用例分析,但之后你可以对 monoidal 做的所有事情就是使用 Monoid API。
在 Haskell 社区中,避免发明事物是一种民间传说,而是更喜欢使用数学和计算机科学(包括函数式编程历史)中的对象。结合 Eq(需要分析 is 参数)和 Monoid 引入了一类新事物 - 支持足够内省以允许相等的 monoid;在这一点上,有一个合理的论点是,一个 Eq-Monoid 的东西违背了它的 Monoid 超类的精神(Monoids 是不透明的)。由于这既是一类新的对象,也可能存在争议——标准实现将不存在。
首先,您的mor
函数看起来相当可疑,因为它需要 aMonoid
但从不使用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
我认为任何使用 of也Default
应该谨慎看待,因为我相信许多 Haskeler 抱怨,这是一个没有原则的类。
我的第二个想法是,您在这里真正处理的数据类型似乎是Maybe (NonEmpty a)
,不是[a]
,而Monoid
您实际上正在谈论的是First
。
import Data.Monoid
morMaybe :: Maybe a -> Maybe a -> Maybe a
morMaybe x y = getFirst (First x <> First y)
然后我们可以将它与列表一起使用,就像在你的例子中一样,在and(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
不是每个幺半群都可以有 的实例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 x
当且当mappend 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]