`RangeExpression`s with exclusive lower bounds

My use-case is meta -- I am trying to write Swift that can properly produce schemas adhering to an existing specification (https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.5) that allows for an exclusive minimum value to be defined. Note that I am writing a type that serializes to a schema following the rules of the JSON Schema specification, not serializing values that need to be valid according to some existing schema (... ignoring the subtlety that the schemas my type serializes can indeed be validated against the JSON Schema meta-schema).

I could come up with a hypothetical reason for needing to say "provide my JSON API with a number strictly greater than x" but I don't have one readily available. I just know that I need to be able to represent that reality because it is stated as part of the specification I am following.

Assuming that we are not debating whether someone could possibly want to write a schema that said "provide me a floating-point value greater than 2.0," I lose information if I try to use Swift RangeExpression types.

Assume I am trying to decode the following schema (and I am going to ignore maximums to focus on the problem being discussed):

{ "type": "number", "minimum": 2.0, "exclusiveMinimum": true }

So I create a type like

struct NumberSchema {
    let range: PartialRangeFrom<Double>
}

and I write my own encoding and decoding functions. The best I can do when decoding the example schema above is to make the range equivalent to 2.0.nextUp.... This will work if I then go to validate some data that is supposed to fit the schema, but I run into problems on encoding because I have thrown away some information. I will produce something like

{ "type": "number", "minimum": 2.0000000000000004, "exclusiveMinimum": false }

This is not accurate, so using an existing RangeExpression type was a non-starter even though in every respect except for representing exclusive-lower-bound there exists a RangeExpression type that is a perfect fit.

No big deal, because I can just make my Swift type more closely resemble the schema. However, now I want to provide the convenience of writing the schema in Swift in the first place using this type (for all the reasons I like writing anything in Swift).

The initializer for this NumberSchema type is worlds more concise if it can take advantage of the expressiveness of built-in RangeExpression operators (as I was describing in my previous comment in this thread).

For what it's worth, I am aware of how niche my use-case is. Nevertheless, it is definitely a real use-case; I was not contriving an example, but rather genuinely found myself wanting to provide a Swift interface I could not write.

For what it's worth, you can still have a type that uses the existing operators for the common cases and has a less pretty form for the full case in the mean time.

2 Likes

Some possible workarounds in the event ease of reading is significantly more important that ease of input:

// U+2024 (One dot leader)
// Warning: Would break if NFKD or NFKC were applied.
infix operator <․․
infix operator <․<

// U+2027 (Hyphenation point)
infix operator <‧‧
infix operator <‧<

// U+22C5 (Dot operator)
infix operator <⋅⋅
infix operator <⋅<

// Or completely overhauling to mimic real mathematical notation:

// U+2212 (minus sign) [as the line segment]
// U+2219 (bullet operator) [as a closed endpoint]
// U+2218 (ring operator) [as an open endpoint]
infix operator ∙−∙
infix operator ∘−∙
infix operator ∙−∘
infix operator ∘−∘

// U+00D7 (multiplication sign) [standing in for the variable “x”]
// U+2264 (less‐than or equal to)
// U+003C (less‐than)
infix operator ≤×≤
infix operator <×≤
infix operator ≤×<
infix operator <×<

Thanks for the suggestions. I'd say for me in this case, "intuitive entry" wins out over "ease of reading." Ideally that would mean the user can specify any possible range using familiar standardized operators, but barring that I think the consistent, albeit much less concise, approach is likely going to be my preference.

[EDIT] that being said, I want to want to use ∙−∙ and its relatives for how visually clean the result is!

An other-side half-open range came up on discussions of linked lists. @tayyab first suggested >**, which I ran with.

I think this operator makes total sense, since you can make the upper bound exclusive, why shouldn’t you be able to do the same to the lower bounds? After all, in most (if not all) cases it would be enough with a ... operator, just less clear. Similarly it is less clear at the lower bound, like the OP mentions, that you want an open range greater than 1, but there is no 1 in the code.

The only reason I can see that we only have an exclusive version for the upper bounds is that you’ll often use this with the count of an array as upper bounds, and they have 0 based indexing.

I think symmetry is a very strong argument here, especially since there won’t be any clashes with existing operators or source breaking changes.

2 Likes

A small digression that mixing count with Index is a little dangerous. It works with Array and only Array (not even ArraySlice). Unless you're sure that you're dealing with Array, now and in the future, it's better to use startIndex and endIndex when subscripting.

The point remains with ..< existing because of startIndex and endIndex. Though it applies more strongly to exclusive upper bound, because of exclusion asymmetry between startIndex and endIndex.

True. Actually my example wasn’t very good. What I think people mostly use Array.count for, certainly I, is not i ranges but in comparisons. If i < views.count. Even then you should probably prefer views.indices.contains(i), but still.

Again, these are not interchangeable for ArraySlice or many other types. If i is an index then the first comparison is incorrect, and if it is an offset from the first element then the second comparison is incorrect. It will work for an Array but, the moment you slice one, this becomes a problem.

Well I am explicitly talking about arrays here. Plus my point was in support of what you are saying now.

While at first I thought "why not just transform 1<..<10 into 2..<10, I realised that you have a use case and that you just as well could argue that 2..<10 may be replaced with 2...9.
This is useful (for some) without adding complexity to the language. If you already understand ..<, you don't need to consult any manual to understand <..<.

1 Like

That's trivial for Strideable types with an integer stride, but not so much for other types.

1 Like

Is there any inherent difference between the lower bound and the upper bound, apart from how they are commonly used? I mean, why would it be harder to exclude the lower bound than the upper, which already works?

Unless there are strong technical reasons, I think the absence of <.. and <.< is quite odd, it's as if we had < but not >.

It's mentioned earlier that operators that contains dots(.) must begin with dots.

Ah, I was actually replying to this, it seems the context was lost:

But regarding the notation question, I'm sure the compiler could make an exception, we already have the ternary operator that is non-standard. OR maybe the requirement can be loosened?

In what way would that be more appropriate? These indicate that your range is smaller than x, which it isn't.

Well of course they are "lesser used", they don't exist! We could get by perfectly well without the normal greater than sign, not to mention "greater than or equal", and "less than or equal", but I think it would require a very strong argument not to have both. At this very basic level I think symmetry should be the default, and you'd need to motivate any asymmetry, rather than the other way around.

1 Like

How they are commonly used is precisely the inherent difference.

They do exist, just not in Swift. They are not commonly used, to my knowledge.

Every addition to the standard library must have compelling motivation. Symmetry is not sufficient; in fact, we explicitly reject additions like isNotEmpty, etc. The bar is even higher for new types, and this idea would require not just one but several. The bar is higher still for new operators, and this idea would add not one but multiple infix and unary operators. The bar is highest for changes to the basic syntax of the language. This idea would require such changes too.

Meh; yeah, I hadn’t really thought through the comment. I was just reacting aesthetically.

Withdrawn.