clamp / median / range
2025-07-02 02:45![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
https://dotat.at/@/2025-07-02-cmp.html
Here are a few tangentially-related ideas vaguely near the theme of comparison operators.
comparison style
Some languages such as BCPL, Icon, Python have chained comparison operators, like
if min <= x <= max:
...
In languages without chained comparison, I like to write comparisons as if they were chained, like,
if min <= x && x <= max {
// ...
}
A rule of thumb is to prefer less than (or equal) operators and avoid greater than. In a sequence of comparisons, order values from (expected) least to greatest.
clamp style
The clamp()
function ensures a value is between some min and max,
def clamp(min, x, max):
if x < min: return min
if max < x: return max
return x
I like to order its arguments matching the expected order of the
values, following my rule of thumb for comparisons. (I used that
flavour of clamp()
in my article about GCRA.) But I seem to
be unusual in this preference, based on a few examples I have
seen recently.
clamp is median
Last month, Fabian Giesen pointed out a way to resolve this
difference of opinion: A function that returns the median
of three values is equivalent to a clamp()
function that doesn't
care about the order of its arguments.
This version is written so that it returns NaN if any of its arguments is NaN. (When an argument is NaN, both of its comparisons will be false.)
fn med3(a: f64, b: f64, c: f64) -> f64 {
match (a <= b, b <= c, c <= a) {
(false, false, false) => f64::NAN,
(false, false, true) => b, // a > b > c
(false, true, false) => a, // c > a > b
(false, true, true) => c, // b <= c <= a
(true, false, false) => c, // b > c > a
(true, false, true) => a, // c <= a <= b
(true, true, false) => b, // a <= b <= c
(true, true, true) => b, // a == b == c
}
}
When two of its arguments are constant, med3()
should compile to the
same code as a simple clamp()
; but med3()
's misuse-resistance
comes at a small cost when the arguments are not known at compile time.
clamp in range
If your language has proper range types, there is a nicer way to make
clamp()
resistant to misuse:
fn clamp(x: f64, r: RangeInclusive<f64>) -> f64 {
let (&min,&max) = (r.start(), r.end());
if x < min { return min }
if max < x { return max }
return x;
}
let x = clamp(x, MIN..=MAX);
range style
For a long time I have been fond of the idea of a simple
counting for
loop that matches the syntax of chained comparisons, like
for min <= x <= max:
...
By itself this is silly: too cute and too ad-hoc.
I'm also dissatisfied with the range or slice syntax in basically every programming language I've seen. I thought it might be nice if the cute comparison and iteration syntaxes were aspects of a more generally useful range syntax, but I couldn't make it work.
Until recently when I realised I could make use of prefix or mixfix syntax, instead of confining myself to infix.
So now my fantasy pet range syntax looks like
>= min < max // half-open
>= min <= max // inclusive
And you might use it in a pattern match
if x is >= min < max {
// ...
}
Or as an iterator
for x in >= min < max {
// ...
}
Or to take a slice
xs[>= min < max]
style clash?
It's kind of ironic that these range examples don't follow the
left-to-right, lesser-to-greater rule of thumb that this post started
off with. (x
is not lexically between min
and max
!)
But that rule of thumb is really intended for languages such as C that don't have ranges. Careful stylistic conventions can help to avoid mistakes in nontrivial conditional expressions.
It's much better if language and library features reduce the need for nontrivial conditions and catch mistakes automatically.