【问题标题】:Test if all elements of a Foldable are the same测试 Foldable 的所有元素是否相同
【发布时间】:2019-04-24 06:47:06
【问题描述】:

我构建了一个函数来验证可折叠结构的所有元素是否相等。

与列表中的类似功能相比,在我看来,更通用的功能异常复杂,但我无法简化它。

你有什么建议吗?

import Data.Monoid
import Data.Sequence as SQ
import Data.Matrix as MT

allElementsEqualL :: Eq a => [a] -> Bool
allElementsEqualL [] = True
allElementsEqualL (x:ns) = all (== x) ns
-- allElementsEqualL [1,1,1] -> True

allElementsEqualF :: (Foldable t, Eq a) => t a -> Bool
allElementsEqualF xs = case (getFirst . foldMap (First . Just) $ xs) of
                        Nothing -> True
                        Just x  -> all (== x) xs

-- allElementsEqualF [1,1,1] -> True

-- allElementsEqualF $ SQ.fromList [1,1,1] -> True

-- allElementsEqualF $ MT.fromLists [[1,1],[1,1]] -> True

【问题讨论】:

  • 当然,你总是可以allElementsEqualF = allElementsEqualL . toList
  • @AlexeyRomanov 我最近想到了这个解决方案,但我认为从类型之间转换的角度来看它可能非常昂贵。如果一切都以“懒惰”的方式发生,也许这将是最方便和最快的解决方案。对吗?
  • @AlexeyRomanov 我认为也是一个混合解决方案:allElementsEqualF2 xs -- | F.null xs = True -- |否则 = all (== x) xs -- 其中 -- x = head $ F.toList xs --- 所以如果 goList 是惰性的,则对原始类型(全部)进行测试。
  • 我认为值得单独回答 :)

标签: haskell


【解决方案1】:

我不知道不太复杂,但我认为这是“最干净”的方式。 “干净”是指使用单个特殊的Monoid 遍历结构。

data Same a = Vacuous | Fail | Same a
instance Eq a => Semigroup (Same a) where
    Vacuous    <> x       = x
    Fail       <> _       = Fail
    s@(Same l) <> Same r  = if l == r then s else Fail
    x          <> Vacuous = x
    _          <> Fail    = Fail
instance Eq a => Monoid (Same a) where
    mempty = Vacuous

allEq :: (Foldable f, Eq a) => f a -> Bool
allEq xs = case foldMap Same xs of
                Fail -> False
                _    -> True

【讨论】:

  • 我认为Samezero 包中的Success a 同构。
  • @ReinHenrichs 我有点怀疑。与a 相比,Same a 有两个额外的构造函数,而Success a 只有一个。现在它可能是Success (Maybe a) 或其他东西......但那时我会说拥有自定义类型更具可读性。
  • 不过,Zero (Same a)zero = Fail 是真的。
  • Same an + 2 元素。 Success a Maybe (Maybe a),它也有n + 2 元素。 (忽略底部。)
  • @ReinHenrichs 嗯。当我点击您的链接时,我看到 newtype Success a = Success { getSuccess :: Maybe a },它只有一个 Maybe 包装器,而不是两个。
【解决方案2】:

第一个函数在第二个函数中不存在的方便之处在于,我们有一种方便的方法来获取列表的“头部”。幸运的是,我们可以为Foldable 做同样的事情。让我们编写一个适用于任何Foldablehead'(为了类型安全,我们将让我们的head' 返回一个Maybe

head' :: (Foldable t, Eq a) => t a -> Maybe a
head' = foldr (\a _ -> Just a) Nothing

现在我们可以编写与一般的列表案例基本相同的代码了。

allElementsEqualF :: (Foldable t, Eq a) => t a -> Bool
allElementsEqualF f = case head' f of
                        Nothing -> True
                        Just a -> all (== a) f

从语法上看,它看起来不同,但它与您在列表案例中所做的完全相同:检查结构是否为空,如果不是,则查看每个元素是否等于第一个元素。

请注意,从技术上讲,这与您发布的代码完全等效,因为它将第一个元素与自身进行比较。因此,如果您的 == 运算符由于某种原因不是自反的,您将得到不同的结果(尝试在列表中运行我的代码和您的代码 [read "NaN" :: Double]

【讨论】:

    【解决方案3】:

    Silvio 的答案在语法上很小且易于理解;但是,如果Foldable 实例不能廉价地计算head',它可能会做与两次折叠相关的额外工作。在这个答案中,我将讨论如何一次性执行计算底层Foldable 是否可以便宜地计算head'

    基本思想是这样的:除了跟踪“到目前为止所有元素是否相等”之外,我们还将跟踪它们都相等。所以:

    data AreTheyEqual a
        = Empty
        | Equal a
        | Inequal
        deriving Eq
    

    这是一个Monoid,以Empty为单位,Inequal为吸收元素。

    instance Eq a => Semigroup (AreTheyEqual a) where
        Empty <> x = x
        x <> Empty = x
        Equal a <> Equal b | a == b = Equal a
        _ <> _ = Inequal
    
    instance Eq a => Monoid (AreTheyEqual a) where
        mempty = Empty
    

    现在我们可以使用foldMap 来概括整个Foldable,如下所示:

    allElementsEqual :: (Eq a, Foldable f) => f a -> Bool
    allElementsEqual = (Inequal /=) . foldMap Equal
    

    【讨论】:

    • 我觉得这和我的回答一样?
    • @HTNW 是的!似乎我们在回答时重叠了。
    • 不一定要做两次遍历。对于 foldr 可以实现受保护递归的结构,它可能第一次只查看最顶层的构造函数。
    • @ReinHenrichs 好点。我会适当地修改我的答案。
    【解决方案4】:

    一个相当微不足道的选择,我通常更喜欢其他答案之一,即重用allElementsEqualL

    allElementsEqualF = allElementsEqualL . toList
    

    或在内联之后

    allElementsEqualF xs = case toList xs of
                             [] -> True
                             x:xs' -> all (== x) xs'
    

    懒惰使它变得合理。 all 调用不需要整个xs',但只需要找到与x 不同的第一个。所以toList 也不会要求整个xs。同时,已经检查过的元素不需要保存在内存中。

    您可以编写一个 Foldable 实例,其中 toList 没有必要那么懒惰,但除了这些情况,我认为它应该做的工作与 Daniel Wagner 和 HTNW 的答案一样多(有轻微的开销,不取决于输入大小)。

    我也想到了一个混合解决方案:

    allElementsEqualF2 xs | F.null xs = True 
                          | otherwise = all (== x) xs 
        where x = head $ F.toList xs 
    

    所以如果 goList 是惰性的,则在原始类型(全部)上进行测试。

    这在非空情况下的工作比 Silvio 的答案稍微多一些,因为 F.nullhead' 的工作完全相同。因此,Silvio 的代码必须到达第一个元素 2 次(一个用于 head',另一个用于 all),而您的代码需要执行 3 次(nullhead $ toList xsall 再次)。

    【讨论】:

    • 有些类型(例如 snoc-lists,或任何其他具有严重左偏树的类型)比 HTNW 和我的解决方案所做的工作要多得多。
    • 我考虑过它们,但认为它最终是一样的,因为 foldMap 仍然需要按顺序迭代它们并且不能利用你的 monoids 是可交换的。没有?
    • 嗯。答案是……比我之前想象的要棘手。 foldMap 可以按照它喜欢的任何顺序进行迭代(因为幺半群必须是关联的),但是 mappend 的实现,特别是它强制其参数的顺序可能会影响实际“访问”的元素数量。回顾我和 HTNW 的回答,我们的mappend 实现在第一个参数中都很严格,这可能会迫使所有工作从左到右完成。老鼠!好的,我不再赞同我之前的评论。
    猜你喜欢
    • 2013-03-10
    • 2010-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-31
    • 1970-01-01
    • 2015-09-14
    相关资源
    最近更新 更多