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 :slight_smile:

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.