8
$\begingroup$

How could an operator to test if a number is within a lower and upper bound? SQL seems to have this as in 10 <= x <= 20. However, if this were to be implemented in a C-style language there would be ambiguity with comparing the boolean/integer resulting from 10 <= x with 20 instead of actually being a proper ternary operator.

Although an algorithm could recognize such ternary between operators and programmers could use parenthesis to bypass them for whatever reason, what are some other syntactical options to implement such a between operator without ambiguity in the first place?

$\endgroup$
2
  • 2
    $\begingroup$ Python, Julia, and some other languages support comparison chaining - you can do x < y < z > k >= i == 2 < a. The lexer probably just lexes it as a "sequence" of comparisons. Besides, there's not much reason to compare 10 <= x and 20. $\endgroup$
    – naffetS
    May 20, 2023 at 0:59
  • $\begingroup$ For what it's worth, I think that syntax and semantics are interleaved in this question, and "implementation", while perhaps not ideal, encompassed that better than purely phrasing it in terms of syntax. $\endgroup$
    – Michael Homer
    May 20, 2023 at 3:24

3 Answers 3

13
$\begingroup$

There are several plausible approaches here, some of which work with the ambiguity and some sidestep it entirely. Some imply significant semantic choices about the language more broadly.

  • A function, or special builtin, like between(x, a, b). This is uninteresting but always practical.

  • A bespoke containment operator: x between 3 and 9. Simple syntax extension.

  • Special parser support for comparison operator chains. This is what Python does, for example: x < y < z tests whether y is within that range rather than being treated as (x < y) < z. In practice, people reading it do understand what's meant without thinking about it and without expecting the parse you mention, and the implementation is straightforward.

  • Disconnect boolean truth from the underlying value. This is something you can do in Raku (Perl 6), though it isn't how its basic comparison operators work: 0 but True is the number zero but also a true value when used as a boolean, and 10 but False is the number ten but also a false value.

    The comparison operator can then return such a value, so 3 < 4 returns a true 4, and 3 < 4 < 2 computes 4 < 2 and returns a false 2. This relies on exactly the (3 < 4) < 2 parse that you identify, but makes it work instead.

  • Distinguish the operator (overload) in use by the expected type in its context. In 2 < x <= 4, the language sees that <= expects a number on the left, and uses an overload of < that returns the right-hand side or short-circuits. Short-circuiting may necessitate a broader semantic change, and overloading by expected type is relatively unusual.

  • Range values that you can test element-of. This is like the mathematical approach of x ∈ (3, 7], so x in range(4, 8) or (2..10).has(x). These may be expected to be iterable, which is awkward for non-integer ranges, and they are clunky for non-numeric types.

  • Mixfix/multi-part operator names. In this case, < ... < would be a single operator, like a Smalltalk method between: and:. This is a pervasive change to the language, rather than a special case.

  • Mixfix function or method names, exactly like Smalltalk: x.between: 3 and: 7. This is also a deep-reaching change, but perhaps somewhat less invasive than for operators.

All of these could be defensible in the right language, but it doesn't seem worthwhile extending a language without mixfix names to have them, for example, just to support range inclusion.

$\endgroup$
2
  • $\begingroup$ "These may be expected to be iterable, which is awkward for non-integer ranges" -- Swift's solution: Range<Bound> is only iterable where Bound: Strideable, Bound.Stride: SignedInteger. Double.Stride == Double, so Range<Double> is not iterable. $\endgroup$
    – Bbrk24
    Jul 2, 2023 at 3:23
  • $\begingroup$ FYI, Python actually implements "element-of" for range and its' various options so you can do 5 in range(1,7,2) == True $\endgroup$
    – kouta-kun
    Jul 2, 2023 at 18:34
3
$\begingroup$

Custom between operator

You could have a two-operand operator, taking a number and a tuple or something like that:

x between (10, 20)

You could also implement it as a ternary just like a ? b : c:

x between 10 and 20

You can of course choose symbols, maybe Unicode, instead of the words between and and. I'm not sure if it would quite fit, but , U+226C is called "BETWEEN".

Another idea:

x ∈ 10..20
$\endgroup$
1
  • 1
    $\begingroup$ Python does this with range: 3 in range(1,5) == True $\endgroup$
    – kouta-kun
    Jul 2, 2023 at 18:10
2
$\begingroup$

I would favor having a comparison operator yield a chainable comparison result which can be implicitly converted to Boolean or used in an overload of the comparison operators. While chained comparisons are most commonly used to check whether one thing is within the range of two others, a single function would suffer from at least three significant limitations:

  1. Sometimes the range should include the right and and sometimes not, and the range should occasionally exclude the left edge.

  2. While checking for ordering among three items may be the most common use case, it's not particularly uncommon to require that four or more items be properly sequenced, e.g. checking whether a <= b, b <= c, and c <= d are all true, but perhaps with some <= replaced with <.

  3. It's also not uncommon to want to check that multiple things are all equal to each other, and for this usage case checking three items would not be particularly more common than checking four. Note that if a compiler wants to include particular logic for it, a multi-way comparison may be faster than a sequence of individual comparisons on some platforms. For example, on an 8-bit platform, checking multiple long values against each other might be done most efficiently by comparing the least-significant byte of every value, and then the next byte of every value, etc.

BTW, an alternative approach to passing code to evaluate the left-hand operand might be to provide a mechanism via which an expression can exit via one of two paths. This would be awkward at the source code level, but could make things nice at the compiler level. If something like:

if (expr1) ...action1...; else ...action2...;

calling an "evaluate branching instruction" with action1 and action2 as the true and false targets, processing something like:

if (expr1 || expr2) ...action1...; else ...action2...;

then pipeOperator(leftExpr, rightExpr, trueAction, falseAction); would involve branchOperator(leftExpr, trueAction, branchOperator(rightExpr, trueAction, falseAction)). It might be possible to process chainable comparison operators similarly.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .