Syntactic sugar for testing a number is in a range?

To my mind,

0 < numberOfApples <= 10

...conveys meaning and reads simpler than:

0 < numberOfApples && numberOfApples <= 10

...because the former closer resembles a bounded value on a number line and saves the repeated symbol name.

I'm wondering if implementing the former as syntactic sugar for the latter is worth pursuing futher? What do you think about it? It seems like there's something wrong with this idea because it does not seem to be implemented in mainstream languages. Is it just me?

3 Likes

Python does this, but it's a hack that doesn't fit that well with static typing, custom operators, etc. The Swift way to write this uses ranges:

(1 ... 10).contains(numberOfApples)
3 Likes

I believe the standard way to write this concept is to use the contains method on ClosedRange.

(0...10).contains(numberOfApples)

I like your proposed natural math syntax, but I'm not sure it would work well within the Swift compiler. Among other things, it might be difficult to parse. It may be easy enough to create a set of rules to parse the proposed syntax, but doing so also may force the compiler to engage in some extra gymnastics when parsing everything else, which could slow compile times across the board. I have no idea whether that would be the case or not. As part of pursuing the discussion of this idea, it would be worthwhile to analyze that aspect of the matter.

Since the range method already works fine, I don’t imagine it would ever be accepted into the Standard Library.


That said, it is easy to implement yourself in a library—well almost. In theory all the building blocks are available. The only hitch is that the Standard Library gets in the way by already defining the comparison operators with no associativity. You cannot shadow a Standard Library operator with your own, nor can you opt out of importing the Standard Library to evade the name collision.

You can get close if you are willing to put up with parentheses:

func <(lhs: Int, rhs: Int) -> (veracity: Bool, checkedValue: Int) {
  return ((lhs < rhs) as Bool, rhs)
}
func <=(lhs: (veracity: Bool, checkedValue: Int), rhs: Int) -> Bool {
  return lhs.veracity && (lhs.checkedValue <= rhs)
}

let numberOfApples = 5
if (0 < numberOfApples) <= 10 {
  // ...
}

Or you can move to your own operators in order to drop the parentheses. Hurray for Unicode!

precedencegroup Inequality {
  lowerThan: ComparisonPrecedence
  associativity: left
}
infix operator ≤: Inequality

func <(lhs: Int, rhs: Int) -> (veracity: Bool, checkedValue: Int) {
  return ((lhs < rhs) as Bool, rhs)
}
func ≤(lhs: (veracity: Bool, checkedValue: Int), rhs: Int) -> Bool {
  return lhs.veracity && (lhs.checkedValue <= rhs)
}

let numberOfApples = 5
if 0 < numberOfApples ≤ 10 {
  // ...
}

(Unicode can only properly help you fix x < y ≤ z, x ≤ y < z, and x ≤ y ≤ z. You cannot express x < y < z unless you invent some more exotic replacement character—such as the “precedes” symbol: ≺.)

3 Likes

You can also use a pattern matching operator instead of contains method

0...10 ~= numberOfApples
18 Likes

I strongly prefer contains or ~= over a new syntax that changes what the <, > etc. operators do.

3 Likes

One point in favour of that syntax is that it's clearer what the bounds are I think. Case in point: one of the examples using contains is off by one.

You also can't write an open lower bound via contains, or can you? That could be a problem for float ranges where you can't easily pick an increment above your boundary to "simulate open".

1 Like

Great point. Off by one, indeed!

For a good discussion of the syntactic obstacle at play in implementing an open lower bound, see this post.

1 Like

Sure, but this is a general problem with ranges in Swift and I don't think inventing new weird special-case syntax for one specific use of a range is the way to address it. If it is worth supporting the full set of range types (and that's historically been unclear without specific use cases) then it should be done via a more general mechanism.

Edit: And I'll note that nextUp makes this pretty easy even for floating point.

1 Like

Oh nice, I had no idea that nextUp exists :+1:

1 Like

Also, Array has the indices property so

if myArray.indices.contains(index) {}

rather than

if index >= 0 && index < myArray.count {}

3 Likes