`Math.clamp` strawman

I believe the most important feature from the Math extensions proposal (last presented in 2016) is the Math.clamp function but the proposal as a whole has since gone quiet. Perhaps it was its amibition for features had eventually caused a roadblock in development.

But regardless, I wanted to take out only the Math.clamp function from the proposal and improve upon it in its own proposal. I'm not sure how the process of finding a champion, or for that matter, getting feedback in the first place goes, but I'll just put this out there.

Proposal: GitHub - Richienb/proposal-math-clamp: ECMAScript proposal and reference implementation for `Math.clamp`.

7 Likes

Could also support BigInt from the get go, if this other proposal advances.

You might want to reach out to @jschoi and see if they would be interested in championing this proposal. Seems like it would be easy to argue that it is motivated.

I am interested in championing (the explainer linked above looks pretty good), but I probably would not have the bandwidth to do so until after at least BigInt Math resolves.

2 Likes

FWIW, it is mathematically the same as a median function with 3 numbers (in any order), so this thread may be related: typedarray.p.[median() | lowerQuartile() | upperQuartile()].

1 Like

They are not the same. Median of 3 must do 3 comparisons, clamp only does 2. Clamp doesn't bother checking whether low <= high, you're supposed to "sort" the boundary arguments. When low > high, clamp returns low (or high, may depend on implementation), not the median.

1 Like

Well, that’s why I said they are mathematically the same. Personally I don’t think returning low or high when low > high is a good idea though.

I was just about to post a proposal for Math.clamp() as well. Not only is it a more human-friendly alternative to Math.min() or Math.max() for clamping operations, it also improves parity with CSS math functions, which already have clamp().

In fact, I find clamp() easier to use even when you only have a minimum or only a maximum, so we may want to handle undefined specially (though that would happen anyway with the current algorithm)

2 Likes

An API that is used with undefined as one of the arguments seems a bit cumbersome, was thinking more like an options bag {min, max}.

Not a bad thought, but I'm discovering a complication in the project I'm working on right now: critical hot-path loops really need to be written in a zero-alloc style to avoid GC churn, and an options bag is an object. I'd need to preallocate the bag, and then usage becomes unwieldy:

const minmaxBag = {min: -Infinity, max: Infinity};
const minBag = {min: -Infinity};
const maxBag = {max: Infinity};

while (highPerformanceLoop) {
    minmaxBag.min = lowerBound;
    minmaxBag.max = upperBound;
    outputValue = Math.clamp(inputValue, minmaxBag);
}

I think I'd prefer a set of three functions:

Math.clamp(value, min, max)
Math.clampToMax(value, max)
Math.clampToMin(value, min)

the latter two are functionally equivalent to Math.min and Math.max (respectively! note the opposite max/min!) but they provide a clearer indication of intent. I know it always feels very weird to me to be writing a Math.min() call when what I'm trying to do is set a maximum for a value; I'd much rather call a function named Math.clampToMax() in that case.

I'm still not a fan of multiple function names here. I don't think the slight increase in clarity is worth the moderate decrease in ease-of-use when something might have a max or a min (or both) at runtime; you have to test and branch if you have multiple functions, versus just setting the min/max bound to null and calling the single function normally.

While using undefined specifically to indicate a missing bound is slightly unwieldy, allowing undefined or null looks a lot better, I think:

Math.clamp(val, min, max)
Math.clamp(val, null, max)
Math.clamp(val, min, null)

For example, assume you're pulling some JSON data which specifies the min and/or max for something. With multiple functions, you have to write:

var clampedVal = data.min == null && data.max == null ? val :
                 data.min == null ? Math.clampToMax(val, data.max) :
                 data.max == null ? Math.clampToMax(val, data.min) :
                 Math.clamp(val, data.min, data.max);

vs

var clampedVal = Math.clamp(val, data.min, data.max);
4 Likes

I like that version better than mine, that gets my vote :smile: the fact that null is representable in JSON makes it even better.

After accepting null/undefined in Math.clamp, I still want to consider keeping the other functions around for syntactic sugar. Specifically, their use case is to be a more explicit formulation of:

// Clamp to an upper bound of 10
Math.min(measurement, 10);

With only Math.clamp, it would be:

Math.clamp(measurement, Number.NEGATIVE_INFINITY, 10);

After:

Math.clampMax(measurement, 10);

I'm inspired by torch.clamp which, being a Python library, allows this sort of magic:

import torch

torch.clamp(x, 0, 10)
torch.clamp(x, min=0)
torch.clamp(x, max=10)

With multiple functions, you have to write: [...]

Surely you'd write

var clampedVal = 
  Math.clamp(val, data.min ?? -Infinity, data.max ?? Infinity);

That really doesn't need an overload.

I like the idea of Math.clamp, but I really do not like the idea of adding new aliases for Math.min and Math.max.

You're using more verbosity here than necessary, and I can't figure out whether that's because you don't understand the null suggestion or because you're trying to complicate the issue to make your idea sound better. With the null/undefined semantics, the proper shorthand way to clamp a value to a maximum of 10 is:

Math.clamp(measurement, null, 10)

which has the same semantics as Math.min(measurement, 10). To my eyes, that's a succinct enough syntax to obviate the need for function aliases. That's obviously a matter of opinion, but if we're going to compare syntaxes, let's compare them on a level playing field.

It's because I've been contributing to repositories that disallow null and prefer Number.POSITIVE_INFINITY and Number.NEGATIVE_INFINITY over Infinity and -Infinity as linter rules. At some point, I cut my losses to avoid having to reconfigure the linter for every one of my own projects.

Then, it's a matter of how you want to reason about your code:

  • Positive/negative infinity if you want to reason that your upper/lower bound is positive/negative infinity
  • undefined if you want to reason that there isn't an upper/lower bound at all.

Actually, now that I'm realizing undefined does have a nice way to reason about, I'm ok with not having the other sugar.

ok, but those are ridiculous linter rules. Null is fine and the globals are a better way to access infinities than the Number constants,

5 Likes

I've rewritten the spec to:

  • Treat null/undefined as "no bound"
  • Change parameter order to (number, min, max), realizing that consistency with CSS is probably not worth it after noticing that all discussion centered around the function uses this new order.
1 Like

Looking for champion!
btw

@Richienb Very interested in championing :)