Swift Digest
SE-0203 | Swift Evolution

Rename Sequence.elementsEqual

Proposal
SE-0203
Authors
Xiaodi Wu
Review Manager
Ted Kremenek
Status
Rejected

01 何が問題だったのか

Sequence プロトコルには、二つのシーケンスを「先頭から順に要素ごとに比較」して一致するか判定する elementsEqual(_:) というメソッドがあります。しかしこの名前は、実際の挙動を正しく伝えておらず、利用者を混乱させる原因になっていました。

==elementsEqual(_:) の結果がずれる

SetDictionary は順序を持たないコレクションですが、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”(順序に依存する操作)として分類されています。しかし、firstlastprefixsuffix のように名前から順序依存だと読み取れる他のメソッドと違い、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が指摘していた「名前から順序依存性が読み取れない」問題自体は解消されていないため、SetDictionary のような順序を持たないコレクションに対して elementsEqual(_:) を使うと直感に反する結果になる可能性がある、という点は、利用者側で引き続き意識しておく必要があります。

実務上は、

  • 二つのコレクションが同じ集合・辞書として等しいかを知りたいときは == を使う。
  • 異なる型のシーケンス間で、先頭から順に要素が一致しているかを見たいときに限って elementsEqual(_:) を使う。

という使い分けを自分で意識することが、現状での回避策になります。