Add clamp(to:) to the stdlib
01 何が問題だったのか
ある値を特定の範囲に収める「クランプ(clamp)」は、座標値をビューの描画領域に収めたり、計算結果を許容範囲内に丸めたりと、日常的に頻出する処理です。値が範囲内ならそのまま、下限より小さければ下限、上限より大きければ上限を返す、という典型的なパターンですが、当時の標準ライブラリには Comparable な値に対する汎用的なクランプ関数が用意されていませんでした。
範囲絞り込みが自前の if 連鎖になってしまう
たとえば座標値を 0 以上 50 以下に収めたいだけの処理でも、次のように上限と下限を個別に比較する定型コードを毎回書く必要がありました。
let clamped: Int
if value > 50 {
clamped = 50
} else if value < 0 {
clamped = 0
} else {
clamped = value
}
三項演算子や min / max の組み合わせで短く書くことはできますが、どの書き方も「範囲で表される制約」と直接対応していないため、意図が伝わりにくく誤りも混入しがちです。
似た操作が CountableRange 側にだけ存在していた
一方で、標準ライブラリには CountableRange の clamped(to:) がすでに存在しており、範囲どうしを重なりに絞り込む操作としてよく使われていました。値側にも同じ発想の clamped(to:) が欲しくなるのは自然なのですが、そのAPIは提供されておらず、概念的な一貫性が欠けていました。値を Comparable の範囲に収める操作も、同じ clamped(to:) という名前で書けるべきだという動機が、この提案の背景です。
02 どのように解決されるのか
Comparable と Strideable のプロトコル拡張として clamped(to:) メソッドを追加し、任意の Comparable な値を範囲に収められるようにします。値が範囲内ならそのまま、下限より小さければ下限、上限より大きければ上限が返ります。
ClosedRange でクランプする
Comparable 拡張として、ClosedRange<Self> を受け取る clamped(to:) が提供されます。
extension Comparable {
func clamped(to range: ClosedRange<Self>) -> Self {
if self > range.upperBound {
return range.upperBound
} else if self < range.lowerBound {
return range.lowerBound
} else {
return self
}
}
}
使い方は直感的で、範囲内なら値そのもの、範囲外なら近い方の境界値が返ります。
100.clamped(to: 0...50) // 50
100.clamped(to: 200...300) // 200
100.clamped(to: 0...150) // 100
半開区間 Range でクランプする(Strideable)
半開区間 Range<Self> でクランプする版は、Stride が整数であるような Strideable の拡張として提供されます。内部では upperBound - 1 を上限とする閉区間に変換してからクランプするため、到達不能な upperBound ではなく「その直前の値」に収まります。
extension Strideable where Stride: Integer {
func clamped(to range: Range<Self>) -> Self {
let clampRange: ClosedRange<Self>
if range.lowerBound == range.upperBound {
clampRange = range.lowerBound...range.upperBound
} else {
clampRange = range.lowerBound...(range.upperBound - 1)
}
return clamped(to: clampRange)
}
}
半開区間版の挙動は次の通りです。
100.clamped(to: 0..<50) // 49
100.clamped(to: 200..<300) // 200
100.clamped(to: 0..<150) // 100
100.clamped(to: 42..<42) // 42
最後の例のように下限と上限が等しい「空の範囲」を渡した場合でも、その境界値(この例では 42)が返るように定義されています。
このProposalの現状
このProposalは Returned for revision となっており、提案された形のままでは標準ライブラリに取り込まれていません。Strideable 拡張の制約(Stride: Integer の前提や半開区間に対する意味づけ)や、浮動小数点数を含む他の Comparable 型との整合性など、詰めるべき設計上の論点が残されていたためです。ダイジェストとしては、この提案が取り扱おうとしていた「Comparable な値に clamped(to:) を提供する」という問題意識と、閉区間・半開区間それぞれに対する具体的な挙動の案を押さえておくと、今後同種の提案を読む際の土台になります。