Подтвердить что ты не робот

Эффективно проверяя, что все элементы (большого) списка одинаковы

Проблема

Предположим, что у нас есть список xs (возможно, очень большой), и мы хотим проверить, что все его элементы одинаковы.

Я придумал различные идеи:

Решение 0

проверяя, что все элементы в tail xs равны head xs:

allTheSame :: (Eq a) => [a] -> Bool
allTheSame xs = and $ map (== head xs) (tail xs)

Решение 1

проверяя, что length xs равна длине списка, полученному с помощью элементов из xs, в то время как они равны head xs

allTheSame' :: (Eq a) => [a] -> Bool
allTheSame' xs = (length xs) == (length $ takeWhile (== head xs) xs)

Решение 2

рекурсивное решение: allTheSame возвращает True, если первые два элемента из xs равны, а allTheSame возвращает True в остальной части xs

allTheSame'' :: (Eq a) => [a] -> Bool
allTheSame'' xs
  | n == 0 = False
  | n == 1 = True
  | n == 2 = xs !! 0 == xs !! 1
  | otherwise = (xs !! 0 == xs !! 1) && (allTheSame'' $ snd $ splitAt 2 xs)
    where  n = length xs

Решение 3

разделите и покорите:

allTheSame''' :: (Eq a) => [a] -> Bool
allTheSame''' xs
  | n == 0 = False
  | n == 1 = True
  | n == 2 = xs !! 0 == xs !! 1
  | n == 3 = xs !! 0 == xs !! 1 && xs !! 1 == xs !! 2
  | otherwise = allTheSame''' (fst split) && allTheSame''' (snd split)
    where n = length xs
          split = splitAt (n `div` 2) xs

Решение 4

Я просто подумал об этом, когда писал этот вопрос:

allTheSame'''' :: (Eq a) => [a] -> Bool
allTheSame'''' xs = all (== head xs) (tail xs)

Вопросы

  • Я думаю, что решение 0 не очень эффективно, по крайней мере, с точки зрения памяти, потому что map построит другой список, прежде чем применять and к его элементам. Я прав?

  • Решение 1 по-прежнему не очень эффективно, по крайней мере, с точки зрения памяти, потому что takeWhile снова создаст дополнительный список. Я прав?

  • Решение 2 является хвостовым рекурсивным (справа?), и оно должно быть довольно эффективным, потому что оно вернет False, как только (xs !! 0 == xs !! 1) будет False. Я прав?

  • Решение 3 должно быть лучшим, поскольку его сложность должна быть O (log n)

  • Решение 4 выглядит для меня довольно Haskellish (это?), но оно, вероятно, такое же, как Solution 0, потому что all p = and . map p (из Prelude.hs). Я прав?

  • Есть ли еще лучшие способы написания allTheSame? Теперь я ожидаю, что кто-то ответит на этот вопрос, сказав мне, что есть встроенная функция, которая делает это: я искал hoogle, и я ее не нашел. Во всяком случае, поскольку я изучаю Haskell, я считаю, что для меня это было хорошим упражнением:)

Любые другие комментарии приветствуются. Спасибо!

4b9b3361

Ответ 1

Ответ gatoatigrado дает хороший совет для измерения производительности различных решений. Вот более символический ответ.

Я думаю, что решение 0 (или, что то же самое, решение 4) будет самым быстрым. Помните, что Haskell ленив, поэтому map не нужно будет создавать весь список до применения and. Хороший способ построить интуицию об этом - играть бесконечно. Так, например:

ghci> and $ map (< 1000) [1..]
False

Это спрашивает, все ли цифры меньше 1000. Если map построил весь список до and, тогда на этот вопрос никогда нельзя было ответить. Выражение будет быстро отвечать, даже если вы дадите списку очень большую правую конечную точку (т.е. Haskell не делает никакой "магии" в зависимости от того, бесконечен ли список).

Чтобы начать мой пример, используйте следующие определения:

and [] = True
and (x:xs) = x && and xs

map f [] = []
map f (x:xs) = f x : map f xs

True && x = x
False && x = False

Вот порядок оценки для allTheSame [7,7,7,7,8,7,7,7]. Будет дополнительный обмен, который является слишком большой болью для записи. Я также буду оценивать выражение head раньше, чем это было бы для краткости (это было бы оценено в любом случае, так что это вряд ли отличается).

allTheSame [7,7,7,7,8,7,7,7]
allTheSame (7:7:7:7:8:7:7:7:[])
and $ map (== head (7:7:7:7:8:7:7:7:[])) (tail (7:7:7:7:8:7:7:7:[]))
and $ map (== 7)  (tail (7:7:7:7:8:7:7:7:[]))
and $ map (== 7)          (7:7:7:8:7:7:7:[])
and $ (== 7) 7 : map (== 7) (7:7:8:7:7:7:[])
(== 7) 7 && and (map (== 7) (7:7:8:7:7:7:[]))
True     && and (map (== 7) (7:7:8:7:7:7:[]))
            and (map (== 7) (7:7:8:7:7:7:[]))
(== 7) 7 && and (map (== 7)   (7:8:7:7:7:[]))
True     && and (map (== 7)   (7:8:7:7:7:[]))
            and (map (== 7)   (7:8:7:7:7:[]))
(== 7) 7 && and (map (== 7)     (8:7:7:7:[]))
True     && and (map (== 7)     (8:7:7:7:[]))
            and (map (== 7)     (8:7:7:7:[]))
(== 7) 8 && and (map (== 7)       (7:7:7:[]))
False    && and (map (== 7)       (7:7:7:[]))
False

Посмотрите, как нам даже не нужно было проверять последние 3 7? Это ленивая оценка, которая делает список более похожим на цикл. Все ваши другие решения используют дорогие функции, такие как length (которые должны пройти весь путь до конца списка, чтобы дать ответ), поэтому они будут менее эффективными, а также они не будут работать в бесконечных списках. Работа над бесконечными списками и эффективностью часто объединяются в Haskell.

Ответ 2

Прежде всего, я не думаю, что вы хотите работать со списками. Многие ваши алгоритмы полагаются на вычисление длины, что плохо. Вы можете рассмотреть пакет vector, который даст вам длину O (1) по сравнению с O (n) для списка. Векторы также намного эффективнее с точки зрения памяти, особенно если вы можете использовать Unboxed или Storable варианты.

При этом вам действительно нужно учитывать обход и шаблоны использования в вашем коде. Списки Haskell очень эффективны, если они могут генерироваться по требованию и потребляться один раз. Это означает, что вы не должны придерживаться ссылок на список. Что-то вроде этого:

average xs = sum xs / length xs

требует, чтобы весь список сохранялся в памяти (либо sum, либо length) до тех пор, пока оба обхода не будут завершены. Если вы можете сделать обход списка за один шаг, это будет намного более эффективно.

Конечно, вам может потребоваться сохранить список в любом случае, например, чтобы проверить, равны ли все элементы, а если нет, сделайте что-то еще с данными. В этом случае со списками любого размера вам, вероятно, будет лучше с более компактной структурой данных (например, вектором).

Теперь, когда это не так, рассмотрим каждую из этих функций. Когда я показываю ядро, он генерируется с помощью ghc-7.0.3 -O -ddump-simpl. Кроме того, не стоит судить о производительности кода Haskell при компиляции с -O0. Скомпилируйте его с помощью флагов, которые вы фактически используете для производственного кода, как правило, как минимум -O и, возможно, другие варианты.

Решение 0

allTheSame :: (Eq a) => [a] -> Bool
allTheSame xs = and $ map (== head xs) (tail xs)

GHC производит это ядро:

Test.allTheSame
  :: forall a_abG. GHC.Classes.Eq a_abG => [a_abG] -> GHC.Bool.Bool
[GblId,
 Arity=2,
 Str=DmdType LS,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
         ConLike=True, Cheap=True, Expandable=True,
         Guidance=IF_ARGS [3 3] 16 0}]
Test.allTheSame =
  \ (@ a_awM)
    ($dEq_awN :: GHC.Classes.Eq a_awM)
    (xs_abH :: [a_awM]) ->
    case xs_abH of _ {
      [] ->
        GHC.List.tail1
        `cast` (CoUnsafe (forall a1_axH. [a1_axH]) GHC.Bool.Bool
                :: (forall a1_axH. [a1_axH]) ~ GHC.Bool.Bool);
      : ds1_axJ xs1_axK ->
        letrec {
          go_sDv [Occ=LoopBreaker] :: [a_awM] -> GHC.Bool.Bool
          [LclId, Arity=1, Str=DmdType S]
          go_sDv =
            \ (ds_azk :: [a_awM]) ->
              case ds_azk of _ {
                [] -> GHC.Bool.True;
                : y_azp ys_azq ->
                  case GHC.Classes.== @ a_awM $dEq_awN y_azp ds1_axJ of _ {
                    GHC.Bool.False -> GHC.Bool.False; GHC.Bool.True -> go_sDv ys_azq
                  }
              }; } in
        go_sDv xs1_axK
    }

На самом деле это выглядит неплохо. Это приведет к ошибке с пустым списком, но это легко исправлено. Это case xs_abH of _ { [] ->. После этого GHC выполнил преобразование рабочего/обертки, функция рекурсивного рабочего является привязкой letrec { go_sDv. Работник исследует свой аргумент. Если [], он достиг конца списка и возвращает значение True. В противном случае он сравнивает главу оставшегося с первым элементом и либо возвращает False, либо проверяет остальную часть списка.

Три других функции.

  • map полностью отключен и не выделяет временную список.
  • В верхней части определения обратите внимание на инструкцию Cheap=True. Это означает, что GHC рассматривает функции "дешево", и, следовательно, кандидат на вложение. При вызове сайта, если конкретный тип аргумента может быть определена, GHC, вероятно, inline allTheSame и создать очень плотная внутренняя петля, полностью обход словаря Eq поиск.
  • Функция рабочего хвостовая рекурсия.

Вердикт: очень сильный соперник.

Решение 1

allTheSame' :: (Eq a) => [a] -> Bool
allTheSame' xs = (length xs) == (length $ takeWhile (== head xs) xs)

Даже не глядя на ядро, я знаю, что это будет не так хорошо. Список перемещается более одного раза, сначала length xs, затем length $ takeWhile. У вас не только дополнительные накладные расходы на множественные обходы, это означает, что список должен сохраняться в памяти после первого обхода и не может быть GC'd. Для большого списка это серьезная проблема.

Test.allTheSame'
  :: forall a_abF. GHC.Classes.Eq a_abF => [a_abF] -> GHC.Bool.Bool
[GblId,
 Arity=2,
 Str=DmdType LS,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
         ConLike=True, Cheap=True, Expandable=True,
         Guidance=IF_ARGS [3 3] 20 0}]
Test.allTheSame' =
  \ (@ a_awF)
    ($dEq_awG :: GHC.Classes.Eq a_awF)
    (xs_abI :: [a_awF]) ->
    case GHC.List.$wlen @ a_awF xs_abI 0 of ww_aC6 { __DEFAULT ->
    case GHC.List.$wlen
           @ a_awF
           (GHC.List.takeWhile
              @ a_awF
              (let {
                 ds_sDq :: a_awF
                 [LclId, Str=DmdType]
                 ds_sDq =
                   case xs_abI of _ {
                     [] -> GHC.List.badHead @ a_awF; : x_axk ds1_axl -> x_axk
                   } } in
               \ (ds1_dxa :: a_awF) ->
                 GHC.Classes.== @ a_awF $dEq_awG ds1_dxa ds_sDq)
              xs_abI)
           0
    of ww1_XCn { __DEFAULT ->
    GHC.Prim.==# ww_aC6 ww1_XCn
    }
    }

Глядя на ядро, он не говорит о многом. Однако обратите внимание на следующие строки:

case GHC.List.$wlen @ a_awF xs_abI 0 of ww_aC6 { __DEFAULT ->
        case GHC.List.$wlen

Здесь происходят переходы списка. Первый получает длину внешнего списка и связывает его с ww_aC6. Второй получает длину внутреннего списка, но привязка не происходит до самого нижнего уровня, в

of ww1_XCn { __DEFAULT ->
GHC.Prim.==# ww_aC6 ww1_XCn

Длины (оба Int s) могут быть распакованы и сравниваться с помощью примитива, но это небольшое утешение после введенных служебных данных.

Вердикт: Нехорошо.

Решение 2

allTheSame'' :: (Eq a) => [a] -> Bool
allTheSame'' xs
  | n == 0 = False
  | n == 1 = True
  | n == 2 = xs !! 0 == xs !! 1
  | otherwise = (xs !! 0 == xs !! 1) && (allTheSame'' $ snd $ splitAt 2 xs)
    where  n = length xs

Это та же проблема, что и решение 1. Список перемещается несколько раз, и он не может быть GC'd. Однако это хуже, потому что теперь длина рассчитывается для каждого подписок. Я ожидаю, что это будет наихудшая производительность всех в списках любого значительного размера. Кроме того, почему вы являетесь специальными списками из 1 и 2 элементов, когда вы ожидаете, что список будет большим?

Вердикт: даже не думайте об этом.

Решение 3

allTheSame''' :: (Eq a) => [a] -> Bool
allTheSame''' xs
  | n == 0 = False
  | n == 1 = True
  | n == 2 = xs !! 0 == xs !! 1
  | n == 3 = xs !! 0 == xs !! 1 && xs !! 1 == xs !! 2
  | otherwise = allTheSame''' (fst split) && allTheSame''' (snd split)
    where n = length xs
          split = splitAt (n `div` 2) xs

Это имеет ту же проблему, что и решение 2. А именно, список проходит несколько раз на length. Я не уверен, что подход "разделяй и властвуй" - хороший выбор для этой проблемы, и это может привести к длительности, чем простое сканирование. Это будет зависеть от данных, хотя, и стоит проверить.

Вердикт: Может быть, если вы использовали другую структуру данных.

Решение 4

allTheSame'''' :: (Eq a) => [a] -> Bool
allTheSame'''' xs = all (== head xs) (tail xs)

Это была моя первая мысль. Позвольте снова проверить ядро.

Test.allTheSame''''
  :: forall a_abC. GHC.Classes.Eq a_abC => [a_abC] -> GHC.Bool.Bool
[GblId,
 Arity=2,
 Str=DmdType LS,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
         ConLike=True, Cheap=True, Expandable=True,
         Guidance=IF_ARGS [3 3] 10 0}]
Test.allTheSame'''' =
  \ (@ a_am5)
    ($dEq_am6 :: GHC.Classes.Eq a_am5)
    (xs_alK :: [a_am5]) ->
    case xs_alK of _ {
      [] ->
        GHC.List.tail1
        `cast` (CoUnsafe (forall a1_axH. [a1_axH]) GHC.Bool.Bool
                :: (forall a1_axH. [a1_axH]) ~ GHC.Bool.Bool);
      : ds1_axJ xs1_axK ->
        GHC.List.all
          @ a_am5
          (\ (ds_dwU :: a_am5) ->
             GHC.Classes.== @ a_am5 $dEq_am6 ds_dwU ds1_axJ)
          xs1_axK
    }

Хорошо, не так уж плохо. Как и решение 1, это будет ошибка в пустых списках. Обход списка скрыт в GHC.List.all, но он, вероятно, будет расширен до хорошего кода на сайте вызова.

Вердикт: Еще один сильный соперник.

Итак, между всеми этими, со списками, я ожидал бы, что решения 0 и 4 будут единственными, стоит использовать, и они почти одинаковы. В некоторых случаях я мог бы рассмотреть вариант 3.

Изменить: в обоих случаях ошибки в пустых списках могут быть просто исправлены, как в ответе @augustss.

Следующим шагом было бы сделать некоторое профилирование времени с помощью criterion.

Ответ 3

Решение с использованием последовательных пар:

allTheSame xs = and $ zipWith (==) xs (tail xs)

Ответ 4

Q1 - Да, я думаю, что ваше простое решение в порядке, утечки памяти нет. Q4 - Решение 3 не является log (n), через простой аргумент, который вам нужен, чтобы посмотреть на все элементы списка, чтобы определить, являются ли они одинаковыми, и просмотр 1 элемента занимает 1 временной шаг. Q5 - да. Q6, см. Ниже.

Путь к этому - ввести его и запустить его

main = do
    print $ allTheSame (replicate 100000000 1)

затем запустите ghc -O3 -optc-O3 --make Main.hs && time ./Main. Мне нравится последнее решение лучше всего (вы также можете использовать сопоставление образцов, чтобы немного почистить его),

allTheSame (x:xs) = all (==x) xs

Откройте ghci и запустите ": step fcn" на этих вещах. Это научит вас многому о том, что ленивая оценка расширяется. В общем случае, когда вы сопоставляете конструктор, например. "x: xs", это постоянное время. Когда вы вызываете "length", Haskell должен вычислять все элементы в списке (хотя их значения все еще "должны быть вычислены" ), поэтому решения 1 и 2 плохие.

изменить 1

Извините, если мой предыдущий ответ был немного мелким. Похоже, что разворачивание вещей вручную помогает немного (хотя по сравнению с другими вариантами, это тривиальное улучшение),

{-# LANGUAGE BangPatterns #-}
allTheSame [] = True
allTheSame ((!x):xs) = go x xs where
    go !x [] = True
    go !x (!y:ys) = (x == y) && (go x ys)

Кажется, что ghc уже специализируется на функции, но вы можете также взглянуть на специализированную прагму, если она не работает для вашего кода [link].

Ответ 5

Вот еще одна версия (не нужно перемещать весь список в случае, если что-то не соответствует):

allTheSame [] = True
allTheSame (x:xs) = isNothing $ find (x /= ) xs

Это может быть не синтаксически правильно, но я надеюсь, что у вас есть идея.

Ответ 6

Вот еще один интересный способ:

{-# INLINABLE allSame #-}
allSame :: Eq a => [a] -> Bool
allSame xs = foldr go (`seq` True) xs Nothing where
  go x r Nothing = r (Just x)
  go x r (Just prev) = x == prev && r (Just x)

Отслеживая предыдущий элемент, а не первый, эту реализацию можно легко изменить для реализации increasing или decreasing. Чтобы проверить их все на первое, вы можете переименовать prev в first и заменить Just x на Just first.


Как это будет оптимизировано? Я не проверял подробно, но я расскажу хорошую историю, основанную на некоторых вещах, которые я знаю о оптимизации GHC.

Предположим сначала, что слияние списков не происходит. Тогда foldr будет вложен, давая что-то вроде

allSame xs = allSame' xs Nothing where
  allSame' [] = (`seq` True)
  allSame' (x : xs) = go x (allSame' xs)

Eta, тогда дает

allSame' [] acc = acc `seq` True
allSame' (x : xs) acc = go x (allSame' xs) acc

Вставка go,

allSame' [] acc = acc `seq` True
allSame' (x : xs) Nothing = allSame' xs (Just x)
allSame' (x : xs) (Just prev) =
  x == prev && allSame' xs (Just x)

Теперь GHC может распознать, что значение Maybe всегда Just для рекурсивного вызова и использует преобразование рабочего-обертки, чтобы воспользоваться этим:

allSame' [] acc = acc `seq` True
allSame' (x : xs) Nothing = allSame'' xs x
allSame' (x : xs) (Just prev) = x == prev && allSame'' xs x

allSame'' [] prev = True
allSame'' (x : xs) prev = x == prev && allSame'' xs x

Помните, что

allSame xs = allSame' xs Nothing

и allSame' больше не являются рекурсивными, поэтому он может быть бета-снижен:

allSame [] = True
allSame (x : xs) = allSame'' xs x

allSame'' [] _ = True
allSame'' (x : xs) prev = x == prev && allSame'' xs x

Таким образом, код более высокого порядка превратился в эффективный рекурсивный код без дополнительного выделения.

Компиляция модуля, определяющего allSame с помощью -O2 -ddump-simpl -dsuppress-all -dno-suppress-type-signatures, приводит к следующему (я немного его очистил):

allSame :: forall a. Eq a => [a] -> Bool
allSame =
  \ (@ a) ($dEq_a :: Eq a) (xs0 :: [a]) ->
    let {
      equal :: a -> a -> Bool
      equal = == $dEq_a } in
    letrec {
      go :: [a] -> a -> Bool
      go =
        \ (xs :: [a]) (prev :: a) ->
          case xs of _ {
            [] -> True;
            : y ys ->
              case equal y prev of _ {
                False -> False;
                True -> go ys y
              }
          }; } in
    case xs0 of _ {
      [] -> True;
      : x xs -> go xs x
    }

Как вы можете видеть, это по сути то же, что и описанный мной результат. Бит equal = == $dEq_a - это то, где метод равенства извлекается из словаря Eq и сохраняется в переменной, поэтому его нужно только один раз извлекать.


Что делать, если происходит слияние списков? Здесь напоминание об определении:

allSame xs = foldr go (`seq` True) xs Nothing where
  go x r Nothing = r (Just x)
  go x r (Just prev) = x == prev && r (Just x)

Если мы назовем allSame (build g), то foldr будет сливаться с build в соответствии с правилом foldr c n (build g) = g c n, что дает

allSame (build g) = g go (`seq` True) Nothing

Это не дает нам ничего интересного, если g не известно. Итак, пусть выбираем что-то простое:

replicate k0 a = build $ \c n ->
  let
    rep 0 = n
    rep k = a `c` rep (k - 1)
  in rep k0

Итак, если h = allSame (replicate k0 a), h становится

let
  rep 0 = (`seq` True)
  rep k = go a (rep (k - 1))
in rep k0 Nothing

Расширяясь,

let
  rep 0 acc = acc `seq` True
  rep k acc = go a (rep (k - 1)) acc
in rep k0 Nothing

Вставка go,

let
  rep 0 acc = acc `seq` True
  rep k Nothing = rep (k - 1) (Just a)
  rep k (Just prev) = a == prev && rep (k - 1) (Just a)
in rep k0 Nothing

Опять же, GHC может видеть, что рекурсивный вызов всегда Just, поэтому

let
  rep 0 acc = acc `seq` True
  rep k Nothing = rep' (k - 1) a
  rep k (Just prev) = a == prev && rep' (k - 1) a
  rep' 0 _ = True
  rep' k prev = a == prev && rep' (k - 1) a
in rep k0 Nothing

Так как rep больше не является рекурсивным, GHC может уменьшить его:

let
  rep' 0 _ = True
  rep' k prev = a == prev && rep' (k - 1) a
in
  case k0 of
    0 -> True
    _ -> rep' (k - 1) a

Как вы можете видеть, это может работать без какого-либо выделения! Очевидно, это глупый пример, но что-то подобное произойдет во многих других интересных случаях. Например, если вы пишете модуль AllSameTest, импортирующий функцию allSame и определяющий

foo :: Int -> Bool
foo n = allSame [0..n]

и скомпилируйте его, как описано выше, вы получите следующее (не очищено).

$wfoo :: Int# -> Bool
$wfoo =
  \ (ww_s1bY :: Int#) ->
    case tagToEnum# (># 0 ww_s1bY) of _ {
      False ->
        letrec {
          $sgo_s1db :: Int# -> Int# -> Bool
          $sgo_s1db =
            \ (sc_s1d9 :: Int#) (sc1_s1da :: Int#) ->
              case tagToEnum# (==# sc_s1d9 sc1_s1da) of _ {
                False -> False;
                True ->
                  case tagToEnum# (==# sc_s1d9 ww_s1bY) of _ {
                    False -> $sgo_s1db (+# sc_s1d9 1) sc_s1d9;
                    True -> True
                  }
              }; } in
        case ww_s1bY of _ {
          __DEFAULT -> $sgo_s1db 1 0;
          0 -> True
        };
      True -> True
    }

foo :: Int -> Bool
foo =
  \ (w_s1bV :: Int) ->
    case w_s1bV of _ { I# ww1_s1bY -> $wfoo ww1_s1bY }

Это может показаться отвратительным, но вы заметите, что нет конструкторов : в любом месте и что Int все распакованы, поэтому функция может работать с нулевым распределением.

Ответ 7

Я думаю, что я мог бы просто реализовать find и переделать этот. Я думаю, что это поучительно, однако, видеть внутренности этого. (Обратите внимание, как решение зависит от того, что равенство является транзитивным, но обратите внимание также, как проблема требует равенства для транзитивности, чтобы быть когерентной.)

sameElement x:y:xs = if x /= y then Nothing else sameElement y:xs
sameElement [x] = Just x
allEqual [] = True
allEqual xs = isJust $ sameElement xs

Мне нравится, как sameElement заглядывает в первые O (1) элементы списка, затем либо возвращает результат, либо рекурсирует на некоторый суффикс списка, в частности хвост. У меня нет ничего умного сказать об этой структуре, мне просто нравится: -)

Я думаю, что я делаю то же самое сравнение, что и this. Если вместо этого я повторил с sameElement x:xs, я бы сравнил головку входного списка с каждым элементом, как в решении 0.

Tangent: можно было бы, если хотите, сообщить два несоответствующих элемента, заменив Nothing на Left (x, y) и Just x на Right x и isJust на either (const False) (const True).