Rename Sequence.elementsEqual
01 何が問題だったのか
Sequence プロトコルには、二つのシーケンスを「先頭から順に要素ごとに比較」して一致するか判定する elementsEqual(_:) というメソッドがあります。しかしこの名前は、実際の挙動を正しく伝えておらず、利用者を混乱させる原因になっていました。
== と elementsEqual(_:) の結果がずれる
Set や Dictionary は順序を持たないコレクションですが、Sequence に適合しているため、elementsEqual(_:) を呼ぶことができます。ところがその結果は、== の結果と一致しないことがあります。
var set1: Set<Int> = Set(1...5)
var set2: Set<Int> = Set((1...5).reversed())
set1 == set2 // true
set1.elementsEqual(set2) // false になり得る
== は「二つのシーケンスが互いに置き換え可能か(substitutable か)」を比較するのに対し、elementsEqual(_:) は「イテレーション順に要素を取り出したときに、それぞれの要素が等しいか」を比較します。Set のような順序を持たないコレクションではイテレーション順が不定なので、同じ内容の集合でも elementsEqual(_:) は false を返すことがあります。
名前から順序依存だと読み取れない
Apple のドキュメントでも elementsEqual(_:) は Set の “order-dependent operation”(順序に依存する操作)として分類されています。しかし、first・last・prefix・suffix のように名前から順序依存だと読み取れる他のメソッドと違い、elementsEqual という名前からはそのことが伝わりません。その結果、ほとんどの場面で == を使うべきところで elementsEqual(_:) が使われてしまう、という間違いが起きやすくなっていました。
一方で、elementsEqual(_:) を単純に削除して == に寄せるわけにもいきません。elementsEqual(_:) は Sequence 上のジェネリックなメソッドとして、UnsafeBufferPointer<Int> と [Int] のような異なる型同士の要素比較にも使えるという、== にはない有用性を持っていたからです。
02 どのように解決されるのか
このProposalでは、混乱の主因が名前そのものにあるという分析に基づき、elementsEqual(_:) を elementsEqualInIterationOrder(_:) に改名することが提案されました。ただし、このProposalは最終的に Rejected(却下) となっており、実際にSwift標準ライブラリに反映されることはありませんでした。以下は提案内容と却下の位置づけの要点です。
提案された改名
既存の elementsEqual(_:) は非推奨扱いにしつつ残し、同じ動作を持つ elementsEqualInIterationOrder(_:) を新設する、という二段構えの変更が想定されていました。
extension Sequence where Element: Equatable {
@available(swift, deprecated: 5, renamed: "elementsEqualInIterationOrder(_:)")
public func elementsEqual<Other: Sequence>(
_ other: Other
) -> Bool where Other.Element == Element {
return elementsEqualInIterationOrder(other)
}
public func elementsEqualInIterationOrder<Other: Sequence>(
_ other: Other
) -> Bool where Other.Element == Element {
// 実装自体は従来の elementsEqual と同じ
// (先頭から順にイテレートして要素ごとに比較する)
...
}
}
同じ趣旨で、カスタム比較を受け取る elementsEqual(_:by:) も elementsEqualInIterationOrder(_:by:) に改名する計画でした。
なぜこの名前だったのか
名前の検討では、lexicographicallyPrecedes(_:) に倣った lexicographicallyMatches や、より直感的な equalsInIterationOrder などの案も検討されました。最終的に elementsEqualInIterationOrder が選ばれた理由は次の通りです。
matchesはパターンマッチを連想させてしまうため不適切。lexicographicallyは一般にはなじみが薄く、要素型がComparableに適合しない場合には正確でもない。- シーケンスそのものの等価(
Sequence.==)ではなく、要素の等価(Sequence.Element.==)に基づく比較であることを明示したいので、elementsの語を残したい。 - 結果として「要素の等価比較を、イテレーション順に行う」ことが名前だけで読み取れる
elementsEqualInIterationOrderが妥当と判断された。
使い方のイメージ
もしこの変更が採用されていたら、利用側のコードは次のように書き換えることが想定されていました。
let a: [Int] = [1, 2, 3]
let b: [Int] = [1, 2, 3]
// 旧: a.elementsEqual(b)
let equal = a.elementsEqualInIterationOrder(b)
// Set 同士の比較のような、順序を問わない比較はそのまま == を使う
let s1: Set<Int> = [1, 2, 3]
let s2: Set<Int> = [3, 2, 1]
let sameSet = (s1 == s2) // true
つまり、「同じ要素を同じ順で持つか」を確かめたいときだけ elementsEqualInIterationOrder(_:)、それ以外は == を使う、という使い分けが明確になるはずでした。
Rejected としての位置づけ
このProposalはレビューを経て Rejected となり、Swiftの標準ライブラリには取り込まれていません。したがって、現在のSwiftでは引き続き Sequence.elementsEqual(_:)(および elementsEqual(_:by:))がそのままの名前で提供されています。Proposalが指摘していた「名前から順序依存性が読み取れない」問題自体は解消されていないため、Set や Dictionary のような順序を持たないコレクションに対して elementsEqual(_:) を使うと直感に反する結果になる可能性がある、という点は、利用者側で引き続き意識しておく必要があります。
実務上は、
- 二つのコレクションが同じ集合・辞書として等しいかを知りたいときは
==を使う。 - 異なる型のシーケンス間で、先頭から順に要素が一致しているかを見たいときに限って
elementsEqual(_:)を使う。
という使い分けを自分で意識することが、現状での回避策になります。