リストxs
(おそらく非常に大きいもの)があり、そのすべての要素が同じであることを確認するとします。
私はさまざまなアイデアを思いつきました:
_tail xs
_のすべての要素が_head xs
_と等しいことを確認します。
_allTheSame :: (Eq a) => [a] -> Bool
allTheSame xs = and $ map (== head xs) (tail xs)
_
_length xs
_が_head xs
_と等しいときに、xs
から要素を取得して取得したリストの長さに等しいことを確認する
_allTheSame' :: (Eq a) => [a] -> Bool
allTheSame' xs = (length xs) == (length $ takeWhile (== head xs) xs)
_
再帰的ソリューション:allTheSame
は、True
の最初の2つの要素が等しい場合に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
_
分割統治:
_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
_
この質問を書いているときに私はこれについて考えました:
_allTheSame'''' :: (Eq a) => [a] -> Bool
allTheSame'''' xs = all (== head xs) (tail xs)
_
map
は要素にand
を適用する前に別のリストを作成するため、ソリューション0は少なくともメモリの点では効率的ではないと思います。私は正しいですか?
takeWhile
が再び追加のリストを作成するため、ソリューション1は、少なくともメモリの点では依然として非常に効率的ではありません。私は正しいですか?
ソリューション2は末尾再帰(正しい?)であり、_(xs !! 0 == xs !! 1)
_がFalseになるとすぐにFalse
を返すため、かなり効率的です。私は正しいですか?
複雑さはO(log n)である必要があるため、ソリューション3が最適です。
ソリューション4は私にはかなりHaskellishのように見えますが(そうですか?)、_all p = and . map p
_(Prelude.hsから)であるため、おそらくソリューション0と同じです。私は正しいですか?
allTheSame
を書く他のより良い方法はありますか?今、誰かがこの質問に答えて、これを行う組み込み関数があることを教えてくれることを期待しています:私はhoogleで検索しましたが、見つかりませんでした。とにかく、私はハスケルを学んでいるので、これは私にとって良い練習だったと思います:)
他のコメントは大歓迎です。ありがとうございました!
gatoatigradoの答えは、さまざまなソリューションのパフォーマンスを測定するための素晴らしいアドバイスです。ここに、より象徴的な答えがあります。
ソリューション0(または完全に同等、ソリューション4)が最速だと思います。 Haskellはlazyであるため、map
を適用する前にand
でリスト全体を作成する必要はありません。これについて直観を構築する良い方法は、無限で遊ぶことです。だから例えば:
ghci> and $ map (< 1000) [1..]
False
これは、すべての数値が1,000未満であるかどうかを尋ねます。 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で一緒に行われることがよくあります。
まず第一に、あなたはリストを扱いたくないと思います。多くのアルゴリズムは長さの計算に依存していますが、これは悪いことです。 vector パッケージを検討すると、O(1)と比較してO(n)の長さが得られます。リスト。特にボックス化されていない、または格納可能なバリアントを使用できる場合は、ベクトルを使用するとメモリ効率が大幅に向上します。
とは言っても、コード内の走査と使用パターンを考慮する必要があります。 Haskellのリストは、オンデマンドで生成して1回使用できる場合、非常に効率的です。つまり、リストへの参照を保持するべきではありません。このようなもの:
average xs = sum xs / length xs
両方の走査が完了するまで、リスト全体を(sum
またはlength
のいずれかによって)メモリに保持する必要があります。リストの走査を1つのステップで実行できる場合は、はるかに効率的です。
もちろん、すべての要素が等しいかどうかを確認するために、とにかくリストを保持する必要がある場合があります。等しくない場合は、データに対して他の処理を行います。この場合、任意のサイズのリストを使用すると、よりコンパクトなデータ構造(ベクトルなど)を使用したほうがよいでしょう。
これで問題は解決しました。次に、これらの各関数について説明します。コアを示すところ、ghc-7.0.3 -O -ddump-simpl
で生成されました。また、-O0を指定してコンパイルするときに、Haskellコードのパフォーマンスを判断しないでください。実稼働コードで実際に使用するフラグを使用してコンパイルします。通常、少なくとも-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を返すか、リストの残りをチェックします。
他の3つの機能。
map
は完全に融合されており、一時リストを割り当てません。Cheap=True
ステートメントに注目してください。これは、GHCが関数を「安い」と見なし、インライン化の候補と見なすことを意味します。呼び出しサイトで、具体的な引数のタイプを決定できる場合、GHCはおそらくallTheSame
をインライン化し、Eq
辞書の検索を完全にバイパスして非常にタイトな内部ループを生成します。評決:非常に強い候補。
ソリューション1
allTheSame' :: (Eq a) => [a] -> Bool
allTheSame' xs = (length xs) == (length $ takeWhile (== head xs) xs)
コアを見ていなくても、これはそれほど良くないことを私は知っています。リストは、最初にlength xs
によって、次にlength $ takeWhile
によってトラバースされます。複数のトラバーサルのオーバーヘッドが増えるだけでなく、リストは最初のトラバーサル後にメモリに保持する必要があり、GCを実行できません。大きなリストの場合、これは深刻な問題です。
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
にバインドします。 2番目は内部リストの長さを取得しますが、バインドは最後の近くまで行われません。
of ww1_XCn { __DEFAULT ->
GHC.Prim.==# ww_aC6 ww1_XCn
長さ(両方のInt
s)はボックス化解除してprimopで比較できますが、これは導入されたオーバーヘッドの後の小さな慰めです。
評決:良くない。
ソリューション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できません。ただし、ここでは長さが各サブリストに対して計算されるため、状況はさらに悪化します。これは、重要なサイズのリストのすべての中で最悪のパフォーマンスになると思います。また、リストが大きくなることが予想されるのに、なぜ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 を使用してしばらくの間プロファイリングを行うことです。
連続ペアを使用するソリューション:
allTheSame xs = and $ zipWith (==) xs (tail xs)
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は不適切です。
以前の答えが少し浅かった場合は申し訳ありません。手動で物事を拡張することは少し助けになるようです(他のオプションと比較して、それは些細な改善です)、
{-# LANGUAGE BangPatterns #-}
allTheSame [] = True
allTheSame ((!x):xs) = go x xs where
go !x [] = True
go !x (!y:ys) = (x == y) && (go x ys)
Ghcはすでに関数を特殊化しているようですが、コードで機能しない場合は、specializeプラグマも確認できます[ link ]。
ここに別のバージョンがあります(何かが一致しない場合にリスト全体を走査する必要はありません):
allTheSame [] = True
allTheSame (x:xs) = isNothing $ find (x /= ) xs
これは構文的に正しくない可能性がありますが、理解していただければ幸いです。
ここに別の楽しい方法があります:
_{-# 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)
_
イータ拡張は
_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
_
したがって、高次のコードは、余分な割り当てのない効率的な再帰コードに変わりました。
_-O2 -ddump-simpl -dsuppress-all -dno-suppress-type-signatures
_を使用してallSame
を定義するモジュールをコンパイルすると、次の結果が得られます(少しクリーンアップしました)。
_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
辞書から抽出され、変数に保存されるため、一度だけ抽出する必要があります。
リストの融合doesが発生した場合はどうなりますか?定義の注意点は次のとおりです。
_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
は、ルールfoldr c n (build g) = g c n
に従ってbuild
と融合し、
_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
sはすべてボックス化されていないため、関数は割り当てなしで実行できます。
find
を実装してやり直し this を行っているだけかもしれません。でも、その内部を見ることは有益だと思います。 (等式が推移的であることにソリューションがどのように依存するかに注意してください、しかし問題が等式が一貫性であるために推移性である必要がある方法にも注意してください)
_sameElement x:y:xs = if x /= y then Nothing else sameElement y:xs
sameElement [x] = Just x
allEqual [] = True
allEqual xs = isJust $ sameElement xs
_
リストの最初のO(1)要素)でsameElement
がピークになり、結果を返すか、リストの一部のサフィックス、特にテールで再帰するのが好きです。私はその構造についてスマートに言うことは何もありません、私はそれが好きです:-)
this と同じ比較を行うと思います。代わりに_sameElement x:xs
_で再帰した場合、ソリューション0のように、入力リストの先頭を各要素と比較します。
正接:必要に応じて、Nothing
をLeft (x, y)
に置き換え、_Just x
_を_Right x
_およびisJust
に置き換えることで、2つの不一致要素を報告できますeither (const False) (const True)
。
あまり効率的ではありませんが(最初の2つの要素が一致しなくてもリスト全体をトラバースします)、ここでは生意気な解決策を示します。
import Data.List (group)
allTheSame :: (Eq a) => [a] -> Bool
allTheSame = (== 1) . length . group
ちょうど楽しみのために。
この実装は優れています。
allSame [ ] = True
allSame (h:t) = aux h t
aux x1 [ ] = True
aux x1 (x2:xs) | x1==x2 = aux x2 xs
| otherwise = False
(==)演算子の推移性を前提として、式のチェーンの等価性を保証する場合にEqのインスタンスが適切に実装されていると想定します(例:a = b = c = d)、a =のみを保証する必要があります。 b、b = c、c = d、およびそのd = a。上記の提供された手法の代わりに、例えばa = b、a = c、a = d、b = c、b = d、c = d。
私が提案した解決策は、テストしたい要素の数とともに直線的に増加します。後者は、効率を向上させるために一定の因子を導入したとしても、二次式です。
最後に長さを使用する必要がないため、グループを使用するソリューションよりも優れています。
ポイントごとに上手に書くこともできますが、そのような些細な詳細に飽きることはありません。