Having a`clamp` function available in the standard library would complement
`min` and `max` really well.
I mentioned this before when the discussion for Swift 4 was still in stage
1, but now that stage 2 has started I thought it might be worth while to
bring up again.
Still needs some work, but here is the draft proposal I wrote previously
Basically it boils down to something like this.
public func clamp<T : Comparable>(value: T, _ lower: T, _ upper: T) -> T {
return max(lower, min(value, upper))
}
IIRC, we’ve been avoiding top-level functions lately… What about as an extension on Range?
extension Range {
func clamp(_ x: Bound) -> Bound {
return max(lowerBound, min(x, upperBound))
}
}
- Dave Sweeris
···
On Mar 9, 2017, at 5:37 PM, Nicholas Maccharoli via swift-evolution <swift-evolution@swift.org> wrote:
Swift Evolution,
Having a`clamp` function available in the standard library would complement `min` and `max` really well.
I mentioned this before when the discussion for Swift 4 was still in stage 1, but now that stage 2 has started I thought it might be worth while to bring up again.
Still needs some work, but here is the draft proposal I wrote previously
Yes, there is no explicit need to make `clamp` a global function, and the
extension you proposed would work just fine I think.
`CountableClosedRange` already provides a `clamped(to:)` method but that
does not suit the use case I was thinking of wanting to receive a single
scalar return value rather than a range.
Any chance of dumping `min` and `max` as global functions?
There is already an extension on `Array` for `min` and `max` but that might
carry the overhead of creating an array every time a `min` or `max` is
performed, unless there was some compiler optimization magic that could
come to the rescue.
- Nick
···
On Fri, Mar 10, 2017 at 10:44 AM, David Sweeris <davesweeris@mac.com> wrote:
On Mar 9, 2017, at 5:37 PM, Nicholas Maccharoli via swift-evolution < > swift-evolution@swift.org> wrote:
Swift Evolution,
Having a`clamp` function available in the standard library would
complement `min` and `max` really well.
I mentioned this before when the discussion for Swift 4 was still in stage
1, but now that stage 2 has started I thought it might be worth while to
bring up again.
Still needs some work, but here is the draft proposal I wrote previously
Yeah I think this works well as an extension on `Comparable`, `foo.clamped(to: 1...100)` seems pretty natural.
Why not go one step further and move the versions of min, max that take two arguments on over to `Comparable` as a protocol extension?
I think that a symmetric operation like `min` or `max` ought to treat both arguments in a symmetric way. `3.max(with: 9)` not only reads badly, but privileges one argument over the other syntactically. I’d very much like to avoid this.
I would be okay with removing top-level min and max if `Array` min and max could generate equivalent code given an array literal. This seems possible.
···
On Mar 9, 2017, at 11:20 PM, Nicholas Maccharoli via swift-evolution <swift-evolution@swift.org> wrote:
Perhaps something like this?
extension Comparable {
func max(with value: Self) -> Self {
if value > self {
return value
}
return self
}
func min(with value: Self) -> Self {
if value < self {
return value
}
return self
}
On Fri, Mar 10, 2017 at 1:41 PM, Nevin Brackett-Rozinsky via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
I’d be on board with an extension of Comparable so you could write “16.clamped(to: 0...10)”. Something along the lines of:
Yeah I think this works well as an extension on `Comparable`, `foo.clamped(to: 1...100)` seems pretty natural.
Why not go one step further and move the versions of min, max that take two arguments on over to `Comparable` as a protocol extension?
I think that a symmetric operation like `min` or `max` ought to treat both arguments in a symmetric way. `3.max(with: 9)` not only reads badly, but privileges one argument over the other syntactically. I’d very much like to avoid this.
Agreed.
I would be okay with removing top-level min and max if `Array` min and max could generate equivalent code given an array literal. This seems possible.
Yeah, it seems like it’d be technically possible, but not without either a lot of compiler magic (not only in somehow optimizing away the overhead of creating an Array, but `[1, 2].max()` returns an `Int?` instead of an `Int`) , or maybe a sufficiently sophisticated macro… The 1st is something we’re trying to avoid, and IIRC the 2nd is out-of-scope for Swift 4. Also, I’m wary of not having the normal “math.h” functions, simply because they’re so universal.
- Dave Sweeris
···
On Mar 10, 2017, at 12:22 AM, Jaden Geller via swift-evolution <swift-evolution@swift.org> wrote:
On Mar 9, 2017, at 11:20 PM, Nicholas Maccharoli via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Yeah I think this works well as an extension on `Comparable`, `foo.clamped(to: 1...100)` seems pretty natural.
Why not go one step further and move the versions of min, max that take two arguments on over to `Comparable` as a protocol extension?
I think that a symmetric operation like `min` or `max` ought to treat both arguments in a symmetric way. `3.max(with: 9)` not only reads badly, but privileges one argument over the other syntactically. I’d very much like to avoid this.
Agreed.
I would be okay with removing top-level min and max if `Array` min and max could generate equivalent code given an array literal. This seems possible.
Yeah, it seems like it’d be technically possible, but not without either a lot of compiler magic (not only in somehow optimizing away the overhead of creating an Array, but `[1, 2].max()` returns an `Int?` instead of an `Int`) , or maybe a sufficiently sophisticated macro… The 1st is something we’re trying to avoid, and IIRC the 2nd is out-of-scope for Swift 4. Also, I’m wary of not having the normal “math.h” functions, simply because they’re so universal.
I suspect the compiler magic wouldn’t be too difficult if the array were a literal, but I don’t know. I forgot that it returns an optional though, that’s definitely problematic and probably a deal-breaker. Perhaps if we ever get vectors (with the size known by the type-system) we could do this (as the result would be non-optional), but I don’t know how it would choose this overload since Array is preferred…
*leaves lid on Pandora’s box and backs away*
···
On Mar 10, 2017, at 1:13 AM, David Sweeris <davesweeris@mac.com> wrote:
On Mar 10, 2017, at 12:22 AM, Jaden Geller via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
On Mar 9, 2017, at 11:20 PM, Nicholas Maccharoli via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Sorry for sidetracking by talking about dumping the global definitions of
`min` and `max` but if that could be done and it were decided by the swift
community that adding a clamp function would be appropriate, I guess with
the array implementations of min / max the clamp function might be
implemented like this?
On Fri, Mar 10, 2017 at 5:22 PM, Jaden Geller <jaden.geller@gmail.com> wrote:
On Mar 9, 2017, at 11:20 PM, Nicholas Maccharoli via swift-evolution < > swift-evolution@swift.org> wrote:
Nevin,
Yeah I think this works well as an extension on `Comparable`,
`foo.clamped(to: 1...100)` seems pretty natural.
Why not go one step further and move the versions of min, max that take
two arguments on over to `Comparable` as a protocol extension?
I think that a symmetric operation like `min` or `max` ought to treat both
arguments in a symmetric way. `3.max(with: 9)` not only reads badly, but
privileges one argument over the other syntactically. I’d very much like to
avoid this.
I would be okay with removing top-level min and max if `Array` min and max
could generate equivalent code given an array literal. This seems possible.
Perhaps something like this?
extension Comparable {
func max(with value: Self) -> Self {
if value > self {
return value
}
return self
}
func min(with value: Self) -> Self {
if value < self {
return value
}
return self
}
I’m ok with doing it as an extension on `Comparable`, although we should add an overload for regular ranges, too.
- Dave Sweeris
···
On Mar 10, 2017, at 1:12 AM, Nicholas Maccharoli via swift-evolution <swift-evolution@swift.org> wrote:
Sorry for sidetracking by talking about dumping the global definitions of `min` and `max` but if that could be done and it were decided by the swift community that adding a clamp function would be appropriate, I guess with the array implementations of min / max the clamp function might be implemented like this?
I’m ok with doing it as an extension on `Comparable`, although we should add an overload for regular ranges, too.
- Dave Sweeris
How would the semantics of that work?
Good questions! I was mostly thinking about how often we do stuff like “for i in 0 ..< arr.count”, and that it’d be handy to not have to switch over to using ClosedRange just to be able to clamp to it. To answer your questions, though…
Should “16.clamped(to: 0..<10)” produce 9 or 10?
9
What about “16.clamped(to: 0..<0)”, which is an empty range?
For `Int`? Crash (which, until about 5 minutes ago, is what I thought would happen if you tried to create a range that’s empty like that). For types that support it, I’d say NaN or something like “nil”/“empty” is the most appropriate return value
Does “16.0.clamped(to: 0..<10)” yield 10.0 or the next-smaller representable Double?
Next-smaller, IMHO. It’s not exactly semantically correct, but AFAIK that’s as correct as Float/Double can be.
Mostly though I’d really like to be able to clamp to array indices, which are pretty much always written as a `Range`, rather than a `ClosedRange`. We could write the function for `Range` to only be generic over `Comparable&Integer`, if the floating point corner cases are too much.
- Dave Sweeris
···
On Mar 10, 2017, at 11:32 AM, Nevin Brackett-Rozinsky via swift-evolution <swift-evolution@swift.org> wrote:
On Fri, Mar 10, 2017 at 4:16 AM, David Sweeris via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
I’m ok with doing it as an extension on `Comparable`, although we should
add an overload for regular ranges, too.
Yeah, I completely agree and for now I'll drop the topic of removing the
global definitions for min / max.
So the aim of this proposal would be to add `clamp` to both `Range` and
`Comparable`.
- Nick
···
On Fri, Mar 10, 2017 at 6:16 PM, David Sweeris <davesweeris@mac.com> wrote:
On Mar 10, 2017, at 1:12 AM, Nicholas Maccharoli via swift-evolution < > swift-evolution@swift.org> wrote:
Sorry for sidetracking by talking about dumping the global definitions of
`min` and `max` but if that could be done and it were decided by the swift
community that adding a clamp function would be appropriate, I guess with
the array implementations of min / max the clamp function might be
implemented like this?
I'm very fond of this proposal. Unless I'm missing more-recent activity, it seems that quite some time has passed since this was last discussed. At the risk of "necroposting" I'd like to ask if there have been any updates regarding the addition of scalar clamping to the standard library. I see the original SE proposal was returned for revisions, but I cannot seem to find any associated commentary.
It is 100% useful, thanks for creating this topic.
I can only add my 5 cents providing a variant of implementation and some examples:
extension Comparable {
public func boundedWith(_ lhs: Self, _ rhs: Self) -> Self {
guard lhs != rhs else { return lhs }
var lower = lhs
var upper = rhs
if rhs < lhs {
swap(&lower, &upper)
}
return min(upper, max(self, lower))
}
}
// The idea is that both
0.boundedWith(5, 1)
// and
0.boundedWith(1, 5)
// return the same result
Examples:
let totalSeconds = seconds.boundedWith(0, Int.max)
let boundedRadius = radius.boundedWith(0, min(bounds.width, bounds.height) / 2)
let amount = amount.boundedWith(0, Double.greatestFiniteMagnitude)
let progressValue = progress.boundedWith(0, 100)
extension CGRect {
public func safeInsetBy(dx: CGFloat, dy: CGFloat) -> CGRect {
let dxSafe = dx.boundedWith(0, width / 2)
let dySafe = dy.boundedWith(0, height / 2)
return insetBy(dx: dxSafe, dy: dySafe)
}
}
func setRating(_ rating: Int) {
let value = rating.boundedWith(1, 5)
// ...
}
I've found myself implementing a clamped(to:) extension of some sort in several different projects, and the existence of the Range.clamped(to:) method always tricks me into thinking it's available on Comparable as well.
Without a strong justification I'd probably want to keep the argument constrained to ClosedRange—is there even a reasonable implementation for clamped(to: Range) on an arbitrary Comparable type? The only one I can see just implicitly includes the upper bound, and makes it redundant with the ClosedRange version.
I believe a version with Range<_> makes sense as well as long as the range is countable. However, with ranges excluding their upperBound, one can form empty ranges though, so we'd need to return an Optional clamped value: