Add a `clamp` function to Algorithm.swift

In my opinion, one more scenario should be covered.

func doSomething(val1: Double, val2: Double) {
  let offset = 10.clamped(to: val1...val2)
}

If val1 > val2, the app will crash with error "Can't form Range with upperBound < lowerBound"
To prevent this, we are forced to check that val1 < val2 every time, which is annoying.

To prevent this, we can make one more clamped(to:) overload:

func clamped(_ lhs: Self, _ rhs) -> 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))
}

What do you think?

You'd have the same issue with any API taking ClosedRange or Range.

Double.random(in: val1...val2)

The solution is simple enough that I don't think it needs additional APIs.

Double.random(in: val1 <= val2 ? val1...val2 : val2...val1)

But you could pitch init(unorderedBounds:) as a separate proposal.

Double.random(in: ClosedRange(unorderedBounds: val1, val2))
2 Likes

Right, this is fundamentally part of how ranges work in Swift, and not something that this API should be working around.

2 Likes

Great idea, I have created a separate pitch: ClosedRange init with unordered bounds

Has there been any progress on this? Would love to have this function available.

1 Like

@John_McCall As mentioned on Github I want to continue the conversation here on this thread before opening a new PR.

I still think this would be a great idea and very useful and there is discussion among community members but not too much back and forth with members of the core team.

Maybe things are a bit more stable now so we can go over this proposal again?

Thanks!

1 Like

That makes sense to me. Feel free to open whenever you think it’s ready. Maybe @scanon can get more eyes from his team on it.

Thoughts on spelling this .bounded(within: some RangeExpression<Self>)?

(Where some RangeExpression<Self> are overloads for every Range type that would make sense to bound a value by, e.g. ClosedRange, PartialRangeFrom, & PartialRangeThrough)

clamped is better than bounded, because bounded does not suggest as clearly what happens for values that are outside the range.

8 Likes

Has this proposal progressed at all since this post? I think these functions would be useful.

1 Like

I just came across something potentially confusing/unexpected about this API.

-10.clamped(to: 0...20) // → -10; since this is evaluated as -(10.clamped(to: 0...20))
(-10).clamped(to: 0...20) // → 0; you need to enclose the '-' for the value to be evaluated as -10

By keeping the API on the Range rather than Comparable you avoid this potentially confusing case ((0...20).clamp(-10)). That said, it probably isn't the common case that you'd be working with literals like that so it would still be nice to see this API on Comparable.

Is anyone willing to revive this proposal?

This is potentially confusing, but it is orthogonal to this API and applies to many members of every integer (and floating-point) type:

-(42.trailingZeroBitCount) // -1
(-42).trailingZeroBitCount //  1
-42.trailingZeroBitCount   // -1

let x = 42
-(x.trailingZeroBitCount)  // -1
(-x).trailingZeroBitCount  //  1
-x.trailingZeroBitCount    // -1

A solution there might involve a warning when negative signs are used with in such manner without parens, but we shouldn't be contorting our API designs on an ad hoc basis because of this orthogonal issue (which is also, if not intuitive, still learnable).

6 Likes

Totally agree, and good to know that this a known/orthogonal behavior.

For completeness' sake, the common use case will probably look more like this:

let x = -42
x.clamped(to: 40...50)	// 40

(Where the negative sign isn't in play at all, rather the value itself is already negative. So the issue doesn't exist.)

I think a warning would actually make sense, since the transformation to disambiguate is both straightforward and self-explanatory. Another pitch, perhaps.

If we can detect when a prefix - is being used in a chain like this and give a warning, why not just allow the - to be part of the value and not an after-the-fact operator?

Seems to me like the real problem is that we're mixing mathematical expressions with logical operations. In a mathematical expression such as -42.clamped(to: 0...20), the - is part of the value "-42" and the . is some weird syntax (IRL in math. is, depending on your locale, only ever a decimal point or thousand's separator, never an operator) for applying the clamped() function. In code, though, the compiler sees things the other way around... the 42.clamped(to: 0...20) is the value and the - is a prefix operator. Could we "just" not have the compiler do that?

That would be a source-breaking change, unfortunately.

It would also be inconsistent with the rest of the language, where the dot operator is considered higher-precedence than prefix operators. E.g. !value.property for booleans and &instance.member for inout arguments.

That being said, the language does already do some magic with negated literals to allow things like let x: Int8 = -128 without overflow errors.

1 Like

Yeah, -x.y means "negate the value of x.y". That's just how that's always been parsed in every Algol-derived language, going back 50+ years.

6 Likes

If I had a time machine, I might require parentheses to disambiguate, but I definitely wouldn't have -x.y mean (-x).y, given the history that John referenced.

5 Likes