Add a `clamp` function to Algorithm.swift


(Nicholas Maccharoli) #1

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

https://github.com/Nirma/swift-evolution/commit/a51c543a76e9a1021996fb4b617311a588e7f397

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))
}

What does the community think?

- Nick


(David Sweeris) #2

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

https://github.com/Nirma/swift-evolution/commit/a51c543a76e9a1021996fb4b617311a588e7f397

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))
}

What does the community think?


(Nicholas Maccharoli) #3

Dave,

Thanks for the feedback!

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

https://github.com/Nirma/swift-evolution/commit/
a51c543a76e9a1021996fb4b617311a588e7f397

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))
}

What does the community think?

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


#4

I’d be on board with an extension of Comparable so you could write
“16.clamped(to: 0...10)”. Something along the lines of:

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

Nevin


(Nicholas Maccharoli) #5

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?

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
    }

    func clamped(to range: ClosedRange<Self>) -> Self {
        let selfUpperMin = range.upperBound.min(with: self)
        return range.lowerBound.max(with: selfUpperMin)
    }
}

- Nick

···

On Fri, Mar 10, 2017 at 1:41 PM, Nevin Brackett-Rozinsky via swift-evolution <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:

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

Nevin

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Jaden Geller) #6

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.

···

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
    }

    func clamped(to range: ClosedRange<Self>) -> Self {
        let selfUpperMin = range.upperBound.min(with: self)
        return range.lowerBound.max(with: selfUpperMin)
    }
}

- Nick

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:

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

Nevin

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Sweeris) #7

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.

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:


(Jaden Geller) #8

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.

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:

- Dave Sweeris


(Nicholas Maccharoli) #9

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?

extension Comparable {

    func clamped(to range: ClosedRange<Self>) -> Self {

        return [range.lowerBound, [self, range.upperBound].min()!].max()!

    }

}

···

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
    }

    func clamped(to range: ClosedRange<Self>) -> Self {
        let selfUpperMin = range.upperBound.min(with: self)
        return range.lowerBound.max(with: selfUpperMin)
    }
}

- Nick

On Fri, Mar 10, 2017 at 1:41 PM, Nevin Brackett-Rozinsky via > swift-evolution <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:

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

Nevin

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Sweeris) #10

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?

extension Comparable {

    func clamped(to range: ClosedRange<Self>) -> Self {

        return [range.lowerBound, [self, range.upperBound].min()!].max()!

    }

}


(David Sweeris) #11

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:


#12

How would the semantics of that work?

Should “16.clamped(to: 0..<10)” produce 9 or 10?

What about “16.clamped(to: 0..<0)”, which is an empty range?

Does “16.0.clamped(to: 0..<10)” yield 10.0 or the next-smaller
representable Double?

Nevin

···

On Fri, Mar 10, 2017 at 4:16 AM, David Sweeris via swift-evolution < 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.

- Dave Sweeris


(Nicholas Maccharoli) #13

​Dave,

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?

extension Comparable {

    func clamped(to range: ClosedRange<Self>) -> Self {

        return [range.lowerBound, [self, range.upperBound].min()!].max()!

    }

}

I’m ok with doing it as an extension on `Comparable`, although we should
add an overload for regular ranges, too.

- Dave Sweeris