 # applyCap and applyThreshold

I'm pitching to introduce two functions to the `protocol Comparable` in swift standard library.

``````extension Comparable {
func applyingCap(_ cap: Self) -> Self {
return min(self, cap)
}

func applyingThreshold(_ threshold: Self) -> Self {
return max(self, threshold)
}
}
``````

The reason for adding these two methods is that the current implementation for applying a limit to a value is not straightforward enough to read. We have a couple of options if we want to implement this logic:

requirement: to limit the variable a to be at least greater than 5

``````var a = 5
a = max(6, a)
``````
``````var a = 5
a = a < 6 ? 6 : a
``````
``````var a = 5
if a < 6 {
a = 6
}
``````

I feel none of the above expressions express clearly "a must be greater than or equal to 6", but `a = a.applyingThreshold(6)` sounds more clear.

In the function proposed, it's not clear to me that "Cap" is related to min and "Threshold" is related to max. It's easier for me to understand the max and min functions already present. But maybe I'm just strange 8 Likes

I feel like you will usually be capping the value both from the bottom and top, so it should be a one method taking a range. That way you can have a nice, unambiguous name "clamp"

``````a.clamp(to: .min...6) // in place
let b = a.clamped(to: 0...6) // returning a new number
``````
7 Likes

Are "cap" and "threshold" terms of art here? If not, I agree with @cloud9999strife that there's no value-add over using names based on "min"/"max".

2 Likes

You could also overload this with partial ranges for the cases where you don't want to clamp both ends - then you'd have a consistent syntax for all such operations:

``````a.clamp(to: ...6) // equivalent to a = min(a, 6)
a.clamp(to: 5...) // equivalent to a = max(a, 5)
``````
15 Likes

I find `min()` and `max()` for clamping are like using `<`/`<=`/`>=`/`>` to compare whether one date is before another one - they both make sense, but in certain situations it isn't intuitive and there's additional cognitive load over `x.clamp(to:)`. Clearly if you want to find the max of two values, `max` makes sense, but if you want to, for example, limit the x coordinate of a sprite:

``````let actualX = min(max(x, 0), 320)
``````

vs

``````let actualX = x.clamp(to: 0...320)
``````

...I think the `clamp` approach is much nicer.

4 Likes

Adding a `clamped(to:)` method to the `Comparable` type has been suggested before, e.g. here: [draft] Add `clamped(to:)` to the stdlib .

1 Like

I guess clamp is from python. It is good. I like it.

Examples from numerical libraries for Python:

Numpy clip

Pytorch clamp

Tensorflow clip_by_value

Mxnet clip

Here's my attempt at implementation, in case anyone wanted to use it.

source code
``````extension Comparable {
func clamped(to range: ClosedRange<Self>) -> Self {
return max(min(self, range.upperBound), range.lowerBound)
}
mutating func clamp(to range: ClosedRange<Self>) {
self = self.clamped(to: range)
}

func clamped(to range: PartialRangeThrough<Self>) -> Self {
return min(self, range.upperBound)
}
mutating func clamp(to range: PartialRangeThrough<Self>) {
self = self.clamped(to: range)
}

func clamped(to range: PartialRangeFrom<Self>) -> Self {
return max(self, range.lowerBound)
}
mutating func clamped(to range: PartialRangeFrom<Self>) {
self = self.clamped(to: range)
}
}

extension Strideable where Stride == Int {
func clamped(to range: Range<Self>) -> Self {
guard !range.isEmpty else {
preconditionFailure("Range cannot be empty")
}
let closedRange = range.lowerBound...range.upperBound.advanced(by: -1)
return self.clamped(to: closedRange)
}
mutating func clamp(to range: Range<Self>) {
self = self.clamped(to: range)
}

func clamped(to range: PartialRangeUpTo<Self>) -> Self {
// I don't think there's a way to check if the range is empty
let closedRange = ...range.upperBound.advanced(by: -1)
return self.clamped(to: closedRange)
}
mutating func clamp(to range: PartialRangeUpTo<Self>) {
self = self.clamped(to: range)
}
}

extension BinaryFloatingPoint {
func clamped(to range: Range<Self>) -> Self {
guard !range.isEmpty else {
preconditionFailure("Range cannot be empty")
}
let closedRange = range.lowerBound...range.upperBound.nextDown
return self.clamped(to: closedRange)
}
mutating func clamp(to range: Range<Self>) {
self = self.clamped(to: range)
}

func clamped(to range: PartialRangeUpTo<Self>) -> Self {
let closedRange = ...range.upperBound.nextDown
return self.clamped(to: closedRange)
}
mutating func clamp(to range: PartialRangeUpTo<Self>) {
self = self.clamped(to: range)
}
}

assert(10.clamped(to: 0...20) == 10)
assert(10.clamped(to: 20...40) == 20)
assert(10.clamped(to: -10...0) == 0)
assert(10.clamped(to: 10...10) == 10)

assert(10.clamped(to: ...20) == 10)
assert(10.clamped(to: ...0) == 0)

assert(10.clamped(to: 0...) == 10)
assert(10.clamped(to: 20...) == 20)

assert(10.clamped(to: ..<10) == 9)
assert(10.clamped(to: 0..<10) == 9)
assert(12.45.clamped(to: ..<0) == -.leastNonzeroMagnitude)
assert(12.45.clamped(to: -10.0..<0) == -.leastNonzeroMagnitude)
assert((10 as UInt).clamped(to: ..<10) == 9)
assert((10 as UInt).clamped(to: 0..<10) == 9)

assert("f".clamped(to: "g"..."z") == "g")
assert("hello!".clamped(to: "a"..."z") == "hello!")
assert("Hello!".clamped(to: "a"..."z") == "a")
assert("".clamped(to: "a"..."z") == "a")
assert("żółć".clamped(to: "a"..."z") == "z")

assert(Double.infinity.clamped(to: 0...20) == 20)
assert(Double.nan.clamped(to: 0...20).isNaN)
assert(10.0.clamped(to: -.infinity...(.infinity)) == 10)
``````
3 Likes

Could the `Stride` get anti-limited to `BinaryInteger` instead of just `Int`? (Actually, it probably has to be `BinaryInteger & SignedNumeric`.)

I would prefer using `clamp` for both operations, by using a half range operator:

``````x.clamp(to: ...top)
x.clamp(to: bottom...)``````
5 Likes

I have no idea how to do that. :)

I think it's the same code, except the "`Stride == Int`" part is

``````Stride: BinaryInteger & SignedNumeric
``````

See if changing that (or maybe just to "`Stride: BinaryInteger`" at first) works.

@CTMacUser `SignedInteger` inherits from `BinaryInteger` and `SignedNumeric`.

@Nicholas_Maccharoli Did you want to revise your SE-0177 proposal? Was it possible to use `RangeExpression` for a generic solution?

As several others have noted, `clip` and `clamp` are the standard terms of art for this operation. (`clip` is used more in signal processing, `clamp` is used in ~every shader and compute language).

3 Likes

Yep! `Stride: BinaryInteger` works

Now what about "`Stride: SignedInteger`," since you're using "-1" in your code.

Also works without any problems

Hi all, adding clamp to the standard lib is also something I'm interested in but looking at the latest posts about it and attempting to revive discussion over at the evolution post [Revision] Fixing SE-0177: Add clamp(to:) to the stdlib it doesn't seem to be moving anywhere unfortunately.

It seems the consensus was to try and go for a protocol oriented approach, however that ran in to some difficulties.

Having played around it with it myself I think there's definitely some design issues that need working out. Specifically, the case of `Int` types and their `Range` and `PartialRangeUpTo` implementations have an unfortunate edge case which is, if you specify a `Range`/`PartialRangeUpTo` where the upper and lower bounds (explicit or implicitly in the case of `PartialRangeUpTo`) are equal, then there is the risk of a runtime error. What's more, this will happen pretty frequently, and in the case of using `clamping(_:)` to 'safely' access elements of an array, a zero length array will cause the very runtime error that was meant to be avoided!

Of course, one could guard against this and just return the lower bound, but this seems imprecise.

With that in mind, I would avoid implementation of clamped for `Range`/`PartialRangeUpTo` specifically for Ints, and I would agree that without support for the full set of ranges, they shouldn't be supported at all.

Here, I would vote to falling back to a simple `clamped(min:,max:_)` free function to sit alongside `min(_:_:)` and `max(_:_:)` in the stdlib.

I do believe there is a case for a clamped function for `FloatingPoint` types, however. This is able to fit the full range of `RangeExpression` types maintaining mathematical integrity (as far as I can tell). It also seems to gel well with the frequent need/use for `clamped` functions in mathematical and computer graphics applications. It also simplifies the implementation.

Terms of Service

Privacy Policy

Cookie Policy