[Revision] Fixing SE-0177: Add clamp(to:) to the stdlib

Swift Evolution,

I want to thank the community for the previous feedback for SE-0177
and also address the issue of the proposal being sent back for revisions.

The current status being "Returned for revisions" reflects the detailed
design section being incomplete and not working against the latest Swift 4
snapshot.

Another reason mentioned by Ben Cohen was that the proposal did not take
advantage of `RangeExpression` and take advantage of a generic
implementation.

This has been added in the "Alternatives Considered" section in the pull
request that
is open now but I personally favor the generic approach despite the added
cost of extending `RangeExpression` enough to make the implementation
possible.

In short, at this time `RangeExpression` is lacking functionality to
implement `clamp(to:)`
in a generic fashion so if the community is in favor of extending
`RangeExpression` I am
more than happy to go with a generic implementation rather than the current
concrete
implementation that is in the proposal pull request now.

I have tried to address these issues and expand the proposal in this pull
request here:

Thank you all so much for the feedback and support, I look forward to
exploring where
we can go with this proposal and making Swift 4 the best release yet.

Forever your Swift Evolution buddy,

ย ย ย - Nick

RangeExpression is a protocol for all ranges, including partial and
unbounded ranges. It would not make sense for such a protocol to have an
upper bound or a lower bound.

The way in which your proposal could dovetail with RangeExpression is for
it to add a requirement to that protocol named `clamping(_:)`. Each type of
range (_, closed, countable, countable closed, partial from, partial up to,
etc.) would then implement that requirement. Then, implementations of
`clamped(to:)` could call `clamping(_:)`. Although, at that point, it would
be an open question whether having both clamped(to:) and clamping(_:) would
be useful. I would be inclined to think that at most one of these is
necessary, and only the latter is compatible with a protocol-based approach.

I remain unconvinced, however, that this API meets the very high bar for
standard library inclusion.

ยทยทยท

On Tue, Jun 27, 2017 at 20:57 Nicholas Maccharoli via swift-evolution < swift-evolution@swift.org> wrote:

Swift Evolution,

I want to thank the community for the previous feedback for SE-0177
and also address the issue of the proposal being sent back for revisions.

The current status being "Returned for revisions" reflects the detailed
design section being incomplete and not working against the latest Swift 4
snapshot.

Another reason mentioned by Ben Cohen was that the proposal did not take
advantage of `RangeExpression` and take advantage of a generic
implementation.

This has been added in the "Alternatives Considered" section in the pull
request that
is open now but I personally favor the generic approach despite the added
cost of extending `RangeExpression` enough to make the implementation
possible.

In short, at this time `RangeExpression` is lacking functionality to
implement `clamp(to:)`
in a generic fashion so if the community is in favor of extending
`RangeExpression` I am
more than happy to go with a generic implementation rather than the
current concrete
implementation that is in the proposal pull request now.

I have tried to address these issues and expand the proposal in this pull
request here:

(Returned for Revisions) [SE-0177] Revise clamping proposal by Nirma ยท Pull Request #723 ยท apple/swift-evolution ยท GitHub

Thank you all so much for the feedback and support, I look forward to
exploring where
we can go with this proposal and making Swift 4 the best release yet.

Forever your Swift Evolution buddy,

   - Nick

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

Xiaodi,

Sorry for the delay and thanks for replying!

Well, as to if this meets the standard thats something more subjective than
objective so
after hearing a few opinions (hopefully differing) the community could
piece together
a cleaner image of if this is a good idea or not.

From the previous feedback on the mailing list there were people in favor

and Ben from the
core swift team gave some feedback here saying:

The core team reviewed this proposal and are returning the proposal for

revision. This is a welcome proposal in principal, but should be revised to
account for the recent addition of the RangeExpression protocol, in order
to allow for clamping over different kinds of range expression.

So since the proposal is being "Returned for revisions" I wanted to propose
those revisions here:

- Nick

ยทยทยท

On Wed, Jun 28, 2017 at 1:13 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

RangeExpression is a protocol for all ranges, including partial and
unbounded ranges. It would not make sense for such a protocol to have an
upper bound or a lower bound.

The way in which your proposal could dovetail with RangeExpression is for
it to add a requirement to that protocol named `clamping(_:)`. Each type of
range (_, closed, countable, countable closed, partial from, partial up to,
etc.) would then implement that requirement. Then, implementations of
`clamped(to:)` could call `clamping(_:)`. Although, at that point, it would
be an open question whether having both clamped(to:) and clamping(_:) would
be useful. I would be inclined to think that at most one of these is
necessary, and only the latter is compatible with a protocol-based approach.

I remain unconvinced, however, that this API meets the very high bar for
standard library inclusion.

On Tue, Jun 27, 2017 at 20:57 Nicholas Maccharoli via swift-evolution < > swift-evolution@swift.org> wrote:

Swift Evolution,

I want to thank the community for the previous feedback for SE-0177
and also address the issue of the proposal being sent back for revisions.

The current status being "Returned for revisions" reflects the detailed
design section being incomplete and not working against the latest Swift
4
snapshot.

Another reason mentioned by Ben Cohen was that the proposal did not take
advantage of `RangeExpression` and take advantage of a generic
implementation.

This has been added in the "Alternatives Considered" section in the pull
request that
is open now but I personally favor the generic approach despite the added
cost of extending `RangeExpression` enough to make the implementation
possible.

In short, at this time `RangeExpression` is lacking functionality to
implement `clamp(to:)`
in a generic fashion so if the community is in favor of extending
`RangeExpression` I am
more than happy to go with a generic implementation rather than the
current concrete
implementation that is in the proposal pull request now.

I have tried to address these issues and expand the proposal in this pull
request here:

(Returned for Revisions) [SE-0177] Revise clamping proposal by Nirma ยท Pull Request #723 ยท apple/swift-evolution ยท GitHub

Thank you all so much for the feedback and support, I look forward to
exploring where
we can go with this proposal and making Swift 4 the best release yet.

Forever your Swift Evolution buddy,

   - Nick

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

Sorry, it's been a while but I don't understand your reply. As far as I can
tell, nothing has changed since June: the revised proposal continues to
propose an API that is not protocol-based, and the alternative continues to
propose additions to RangeExpression that do not make sense, as partial
ranges do not have both upper and lower bounds.

As I wrote in June, _even if clamping is determined to be a suitable
addition to the standard library_, your revision appears not to address the
core team feedback. I suggested one way in which you could revise your
proposal to address that issue--namely, a `clamping` requirement on
RangeExpression. However, I do not see where you have addressed this
feedback. What am I missing?

ยทยทยท

On Wed, Jul 19, 2017 at 22:48 Nicholas Maccharoli <nmaccharoli@gmail.com> wrote:

Xiaodi,

Sorry for the delay and thanks for replying!

Well, as to if this meets the standard thats something more subjective
than objective so
after hearing a few opinions (hopefully differing) the community could
piece together
a cleaner image of if this is a good idea or not.

From the previous feedback on the mailing list there were people in favor
and Ben from the
core swift team gave some feedback here saying:

Update 0177-add-clamped-to-method.md by airspeedswift ยท Pull Request #718 ยท apple/swift-evolution ยท GitHub

The core team reviewed this proposal and are returning the proposal for

revision. This is a welcome proposal in principal, but should be revised to
account for the recent addition of the RangeExpression protocol, in
order to allow for clamping over different kinds of range expression.

So since the proposal is being "Returned for revisions" I wanted to
propose those revisions here:

(Returned for Revisions) [SE-0177] Revise clamping proposal by Nirma ยท Pull Request #723 ยท apple/swift-evolution ยท GitHub

- Nick

On Wed, Jun 28, 2017 at 1:13 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

RangeExpression is a protocol for all ranges, including partial and
unbounded ranges. It would not make sense for such a protocol to have an
upper bound or a lower bound.

The way in which your proposal could dovetail with RangeExpression is for
it to add a requirement to that protocol named `clamping(_:)`. Each type of
range (_, closed, countable, countable closed, partial from, partial up to,
etc.) would then implement that requirement. Then, implementations of
`clamped(to:)` could call `clamping(_:)`. Although, at that point, it would
be an open question whether having both clamped(to:) and clamping(_:) would
be useful. I would be inclined to think that at most one of these is
necessary, and only the latter is compatible with a protocol-based approach.

I remain unconvinced, however, that this API meets the very high bar for
standard library inclusion.

On Tue, Jun 27, 2017 at 20:57 Nicholas Maccharoli via swift-evolution < >> swift-evolution@swift.org> wrote:

Swift Evolution,

I want to thank the community for the previous feedback for SE-0177
and also address the issue of the proposal being sent back for
revisions.

The current status being "Returned for revisions" reflects the detailed
design section being incomplete and not working against the latest Swift
4
snapshot.

Another reason mentioned by Ben Cohen was that the proposal did not take
advantage of `RangeExpression` and take advantage of a generic
implementation.

This has been added in the "Alternatives Considered" section in the pull
request that
is open now but I personally favor the generic approach despite the
added cost of extending `RangeExpression` enough to make the implementation
possible.

In short, at this time `RangeExpression` is lacking functionality to
implement `clamp(to:)`
in a generic fashion so if the community is in favor of extending
`RangeExpression` I am
more than happy to go with a generic implementation rather than the
current concrete
implementation that is in the proposal pull request now.

I have tried to address these issues and expand the proposal in this
pull request here:

(Returned for Revisions) [SE-0177] Revise clamping proposal by Nirma ยท Pull Request #723 ยท apple/swift-evolution ยท GitHub

Thank you all so much for the feedback and support, I look forward to
exploring where
we can go with this proposal and making Swift 4 the best release yet.

Forever your Swift Evolution buddy,

   - Nick

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

Hi, is there an update on this proposal? Is this something that needs to be completed before ABI lock-in or can it wait?

@Douglas_Gregor commented on the PR at (Returned for Revisions) [SE-0177] Revise clamping proposal by Nirma ยท Pull Request #723 ยท apple/swift-evolution ยท GitHub saying it was put on ice pending further discussion but I haven't found anything further.

It would be great to see this included โ€“ It's something that I seem to encounter on a near daily basis.

I've attempted my own version with the suggestions of @xwu. A deviation from the original proposal is to constrain the ClosedRange and PartialRangeUpTo variants to FloatingPoint types. clamping to something like (0..<0) would be undefined anyhow, but FloatingPoint at least allows us to return .nan in this case โ€“ so I think this might make more sense than Strideable which doesn't seem to have the concept of a minimum stride as far as I can tell.

// Comparable extension

extension Comparable {
    func clamped<T: ClampableRangeExpression>(to range: T) -> Self where T.Bound == Self {
        return range.clamping(self)
    }
}

// RangeExpression extension

protocol ClampableRangeExpression: RangeExpression {
    func clamping(_ value: Bound) -> Bound
}

// Conformance for various Range Types

extension PartialRangeFrom: ClampableRangeExpression {
    func clamping(_ value: Bound) -> Bound {
        return contains(value) ? value : lowerBound
    }
}

extension PartialRangeThrough: ClampableRangeExpression {
    func clamping(_ value: Bound) -> Bound {
        return contains(value) ? value : upperBound
    }
}

extension ClosedRange: ClampableRangeExpression {
    func clamping(_ value: Bound) -> Bound {
        return (lowerBound...).clamping((...upperBound).clamping(value))
    }
}

// Constrained conformance for relevant Range Types

extension PartialRangeUpTo: ClampableRangeExpression where Bound: FloatingPoint {
    func clamping(_ value: Bound) -> Bound {
        return (...upperBound.nextDown).clamping(value)
    }
}

extension Range: ClampableRangeExpression where Bound: FloatingPoint {
    func clamping(_ value: Bound) -> Bound {
        guard lowerBound != upperBound else { return .nan }
        return (lowerBound...).clamping((..<upperBound).clamping(value))
    }
}

The alternative is the simple:

public func clamped<T>(_ lowerBound: T, _ upperBound: T, _ value: T) -> T where T : Comparable {
    return min(upperBound, max(lowerBound, value))
}

Which seems like it would fit alongside the existing min and max implementations quite neatly to me, but I understand there has been some discussion which discourages this direction.

Either way, it would be good to get a solution of some sort.

Looking a little further, for the the Range and PartialRangeUpTo variants it would in fact make sense to constrain them to Strideable. However, it does seem we also need the concept of .nextDown from Decimal added to the Ints.

Perhaps a new protocol, Decrementable could be created which can then have all the Ints, Decimals, etc. conforming to it.

This would allow for:



extension Range: ClampableRangeExpression where Bound: Strideable & Decrementable {
    func clamping(_ value: Bound) -> Bound {
        return (lowerBound...).clamping((..<upperBound).clamping(value))
    }
}

extension PartialRangeUpTo: ClampableRangeExpression where Bound: Strideable & Decrementable {
    func clamping(_ value: Bound) -> Bound {
        return (...upperBound.nextDown).clamping(value)
    }
}

// Decrementable protocol

protocol Decrementable {
    var nextDown: Self { get }
}

// Conformance for all the Decimals...

extension Decimal: Decrementable {}
extension CGFloat: Decrementable {}

// ...

// Conformance for all the Ints...

extension Int: Decrementable {
    var nextDown: Int { return advanced(by: -1) }
}

// ...

Hi, it doesn't look like there is much activity on this โ€“ but for the record:

Having thought about this further and playing 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.

Hi all, at risk of spamming the board, is there no longer an appetite for this change anymore? Does it need a new advocate? Or is it just on hold?

I am currently rewriting sections of the proposal and the detailed design for this.
I should be done soon.

3 Likes