Swift Digest
SE-0051 | Swift Evolution

Conventionalizing stride semantics

Proposal
SE-0051
Authors
Erica Sadun
Review Manager
Status
Withdrawn

01 何が問題だったのか

Swift 2.2 時点の stride(to:by:) / stride(through:by:) は、関数名と実際のふるまいの対応がずれていて直感的ではありませんでした。辞書的な意味から期待される挙動と、実際に生成される数列が一致していなかったのです。

to は「終端に到達しない」

to: で指定した終端は半開区間 [start, end) の右端として扱われ、数列に含まれません。たとえば以下の式では 4 は出てきません。

Array(1.stride(to: 4, by: 1))
// [1, 2, 3]

英語の to は「範囲の終端として到達する点」を表すのに、実際には終端 に到達しない ため、名前と挙動が噛み合っていません。

through は「通り抜けない」

through: で指定した終端は閉区間 [start, end] の右端として扱われますが、刻み幅の都合で最後の値がぴったり終端にならない場合、それ以上進まずに止まります。名前から受ける「終端を通過する」という印象と違い、終端の手前で止まるだけで、通り抜けはしません。

Array(1.stride(through: 10, by: 8))
// [1, 9]   ← 10 を通り抜けない

「通り抜ける」挙動にしたい場合は、既存の to / through のどちらを使っても表現できず、別途ループを書く必要がありました。

3 つのスタイルに対応する名前が足りない

整理すると、ストライドで表したい半直線上の範囲にはもともと 3 種類の使い分けがあります。

  • [start, end): 終端は含めず手前で止める
  • [start, end]: 終端は含めうるが越えない
  • [start, ≥end]: 終端を必ず通り抜ける(終端と一致するか、終端を越えて止まる)

このうち Swift 2.2 の API では 1 番目と 2 番目しか表現できず、しかもそれぞれに割り当てられた名前が辞書的な意味とずれていた、というのがこの提案の問題意識です。

02 どのように解決されるのか

この提案は Withdrawn(取り下げ) となりました。ここで示されたリネームや新しいスタイルの追加は Swift に入っていません。以下では「もし採択されていたら何がどう変わるはずだったか」を整理します。

既存 2 種類のリネームと、3 種類目の追加

提案の骨子は、既存の 2 つのスタイルに対する名前を付け直し、さらに新しいスタイルを 1 つ足すというものでした。

  • to[start, end))→ towards にリネーム。各要素が終端に向かって「近づいていく」ことを表します。
  • through[start, end])→ to にリネーム。終端に「到達しうる」ことを表します(ただし刻み幅によっては一致しない場合もあります)。
  • through[start, ≥end])を追加。最終値が終端と一致するか、終端を越えて止まる、つまり終端を 通り抜ける ことを保証します。

対応する呼び出しは次のイメージです。

Array(1.stride(towards: 10, by: 8))   // 旧 to 相当
// [1, 9]

Array(1.stride(to: 10, by: 8))        // 旧 through 相当
// [1, 9]

Array(1.stride(through: 10, by: 8))   // 新しい through
// [1, 9, 17]   ← 10 を通り抜けて止まる

既存の 2 スタイルについては、挙動そのものは一切変えず、名前だけを付け替えるという整理でした。

新しい through の止まり方

through では、内部のイテレータが「現在値が終端を超えた時点の値を最後に 1 つ返してから止まる」動作をします。提案に示されたスケッチは次のような形です(コード中の識別子はバッククォートなしで表記します)。

public mutating func next() -> Element? {
    if done {
        return nil
    }
    if stride > 0 ? current >= end : current <= end {
        done = true
        return current
    }
    let result = current
    current = current.advancedBy(stride)
    return result
}

刻み幅が正なら「current が end 以上になった時点で最後に一つ返す」、負なら「current が end 以下になった時点で最後に一つ返す」という対称的な形です。結果として最終値は必ず end 以上(刻み幅が負なら以下)、かつ end + stride 未満に収まります。

浮動小数点への副次的な効果

through は、浮動小数点ストライドで問題になっていた「最後の値が終端に届かない」現象への回避策としても働く想定でした。累積誤差で最終値がわずかに終端を下回ってしまう場合でも、新 through なら「終端を通り抜けるまで」回すため、終端付近の値が確実に含まれます。

// 新 through での挙動(提案採択後を想定)
Array(1.0.stride(through: 2.0, by: 0.1))
// [1.0, 1.1, 1.2, ... , 1.9, 2.0]

Array(1.0.stride(through: 1.9, by: 0.25))
// [1.0, 1.25, 1.5, 1.75, 2.0]   ← 1.9 を通り抜けて止まる

ただしこれは浮動小数点の累積誤差そのものを解決するものではなく、あくまで「終端を取りこぼさない」ための仕組みです。累積誤差自体は別の問題で、SE-0050 で扱われていました。

現在の Swift での立ち位置

この提案は取り下げられ、to / through のリネームも、新しい through の意味づけも Swift には導入されませんでした。現在の Swift における stride(to:by:) / stride(through:by:) は、名前もふるまいも Swift 2.2 当時と同じ [start, end) / [start, end] のままです。「終端を必ず通り抜ける」ストライドが必要な場合は、現状では呼び出し側で end を刻み幅ぶん広げるか、明示的にループを書いて対応する必要があります。あくまで「ストライドの名前と意味のずれをどう整理しようとしていたか」を知るための歴史的な提案として位置づけられます。