10

I built a function that verifies that all elements of a foldable structure are equal.

Compared to a similar function on the lists, it seems to me that the more general function is disproportionately complex, but I have not been able to simplify it.

Do you have any suggestions?

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
Alberto Capitani
  • 1,039
  • 13
  • 30
  • 1
    Of course, you can always do `allElementsEqualF = allElementsEqualL . toList`. – Alexey Romanov Apr 23 '19 at 17:35
  • @AlexeyRomanov I recently thought of this solution, but I thought it could be very expensive from the point of view of conversion between types. If instead everything happened in a "lazy" way, maybe it would be the most convenient and fastest solution. Is it correct? – Alberto Capitani Apr 23 '19 at 18:32
  • @AlexeyRomanov I thought also a mixed solution: allElementsEqualF2 xs -- | F.null xs = True -- | otherwise = all (== x) xs -- where -- x = head $ F.toList xs --- so if goList is lazy, the test is carried out upon the original type (with all). – Alberto Capitani Apr 23 '19 at 18:38
  • I decided it was worth a separate answer after all :) – Alexey Romanov Apr 23 '19 at 20:15

4 Answers4

13

I don't know about less complicated, but I think this is the "cleanest" way to do it. By "clean," I mean it's one traversal over the structure using a single, special 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
HTNW
  • 27,182
  • 1
  • 32
  • 60
  • I think `Same` is isomorphic to `Success a` from the [zero](http://hackage.haskell.org/package/zero-0.1.4/docs/Data-Zero.html) package. – Rein Henrichs Apr 23 '19 at 17:07
  • 1
    @ReinHenrichs I kind of doubt it. `Same a` has two extra constructors compared to `a`, whereas `Success a` has just one. Now it might be `Success (Maybe a)` or something... but at that point I would say having a custom type is more readable. – Daniel Wagner Apr 23 '19 at 17:08
  • It is, however, true that `Zero (Same a)` with `zero = Fail`. – HTNW Apr 23 '19 at 17:11
  • `Same a` has `n + 2` elements. `Success a` *is* `Maybe (Maybe a)`, which also has `n + 2` elements. (Ignoring bottoms.) – Rein Henrichs Apr 23 '19 at 17:12
  • @ReinHenrichs Hm. When I follow your link, I see `newtype Success a = Success { getSuccess :: Maybe a }`, which has just one `Maybe` wrapper, not two. – Daniel Wagner Apr 23 '19 at 17:13
  • Ah, you're right. Whoops! You do need to track the elements plus one extra bit of information (whether you've seen an element and failed to match, or whether you've just not seen an element yet.) So the structure you're looking for should have `n + 2` elements. – Rein Henrichs Apr 23 '19 at 17:13
  • I love those bring-your-own-monoid solutions :) – duplode Apr 23 '19 at 22:59
  • @HTNW I appreciate your solution. I would like to ask you a question that is a little off the subject. The Same semigroup is commutative; Given this, is there a way to indicate this property and to make Haskell (or GHC) automatically expand the symmetrical part? – Alberto Capitani Apr 24 '19 at 07:10
  • @AlbertoCapitani Do you want a catch-all equation `x <> y = y <> x`? – HTNW Apr 24 '19 at 07:27
  • @HTNW Yes, it would be fine. Ex. ::Com (Vacuous <> x = x) should be interpreted (expanded) (by GHC?) as: Vacuous <> x = x; x <> Vacuous = x. – Alberto Capitani Apr 24 '19 at 09:06
  • @AlbertoCapitani No, I mean you can literally replace the last two equations with `x <> y = y <> x` and have it work, right? – HTNW Apr 24 '19 at 10:51
  • @HTNW Why if I replace the two Vacuous occurences in (Semigroup(Same)'s instance with 1 line: "Vacuous <> x = x <> Vacuous" the compiler give me an error? ("Non-exhaustive patterns in function <>") – Alberto Capitani Apr 24 '19 at 12:46
  • @AlbertoCapitani Because the patterns aren't exhaustive if you write it like that? This is not something you can be clever with. The LHSs need to cover all patterns, period, no matter what the RHSs are. You can write `Vacuous <> x = x; Fail <> x = Fail; s@(Same l) <> Same r = if l == r then s else Fail; x <> y = y <> x` if you want to show commutativity. That's the best I can think up. – HTNW Apr 24 '19 at 15:35
  • @HTNW Thank you very much. – Alberto Capitani Apr 24 '19 at 16:01
  • One issue with this is that `allEqTo $ 2 : (cycle [1])` does not terminate - a rather exotic case, though. – mcmayer Nov 10 '20 at 08:00
7

The convenient thing about your first function that doesn't exist in your second is that we have a convenient way of getting the "head" of a list. Fortunately, we can do the same for a Foldable. Let's write a head' that works on any Foldable (and for the sake of type safety we'll have our head' return a Maybe)

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

Now we can write basically the same code as the list case for the general one.

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

Syntactically, it looks different, but it's the exact same thing you did in your list case: check if the structure is empty and, if it's not, then see if every element is equal to the first.

Note that, technically, this is not quite equivalent to the code you posted, as it compares the first element against itself. So if your == operator is for some reason not reflexive, you'll get different results (try running my code and yours on the list [read "NaN" :: Double])

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
6

Silvio's answer is syntactically small and easy to understand; however, it may do extra work associated with doing two folds if the Foldable instance can't compute head' cheaply. In this answer I will discuss how to perform the calculation in just one pass whether the underlying Foldable can compute head' cheaply or not.

The basic idea is this: instead of tracking just "are all the elements equal so far", we'll also track what they're all equal to. So:

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

This is a Monoid, with Empty as the unit and Inequal as an absorbing element.

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

Now we can use foldMap to summarize an entire Foldable, like so:

allElementsEqual :: (Eq a, Foldable f) => f a -> Bool
allElementsEqual = (Inequal /=) . foldMap Equal
Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
4

A rather trivial option, and I would generally prefer one of the other answers, is reusing allElementsEqualL:

allElementsEqualF = allElementsEqualL . toList

or after inlining

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

It's laziness which makes it reasonable. The all call doesn't demand the entire xs', but only until it finds the first one different from x. So toList will also not demand the entire xs. And at the same time, already examined elements don't need to be kept in memory.

You could write a Foldable instance for which toList is less lazy than necessary, but except for those cases I think it should do exactly as much work as Daniel Wagner's and HTNW's answer (with slight overhead not depending on input size).

I thought also a mixed solution:

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

so if goList is lazy, the test is carried out upon the original type (with all).

This does slightly more work in the non-empty case than Silvio's answer, because F.null duplicates exactly as much of F.toList's work as head' does. So Silvio's code has to get to the first element 2 times (one for head' and another inside all), and yours does it 3 times (null, head $ toList xs and all again).

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • There are types (such as snoc-lists, or any other type with heavily left-biased trees) where this will do significantly more work than HTNW's and my solution. – Daniel Wagner May 12 '19 at 14:01
  • I considered them, but thought it ends up being the same because foldMap is still required to iterate over them in order and can't take advantage of your monoids being commutative. No? – Alexey Romanov May 13 '19 at 14:04
  • Hmmm. The answer is... trickier than I was thinking it was before. `foldMap` may iterate in any order it likes (because monoids are required to be associative), but the implementation of `mappend` and in particular which order it forces its arguments in may affect how many elements are actually "visited". Reviewing my and HTNW's answer, both our `mappend` implementations are strict in their first argument, which probably forces all the work to be done left-to-right. Rats! Okay, I no longer endorse my previous comment. – Daniel Wagner May 13 '19 at 14:21