fanf: (Default)
[personal profile] fanf

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.

July 2025

S M T W T F S
  1 2345
6789101112
13141516171819
20212223242526
2728293031  

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated 2025-07-06 11:18
Powered by Dreamwidth Studios