Actually one invariant that Interval
does enforce that tuples do not is that both elements must be the same type, and that type must be FloatingPoint
. Another reason why I have been happily using Interval
in my own code for a number of years as opposed to using a tuple is that it provides a layer of abstraction that tuples do not provide. Every attribute that Interval
offers, like isEmpty
would have to be explicitly coded every time it is needed using tuples. Linear interpolation functions could be provided that take tuples, yes, but the clarity of using the ..
operator to construct a definite type as opposed to an undifferentiated tuple makes the code much easier to understand and debug.
I am contrasting to a loss in performance by performing constructiontime checks to ensure invariants like a
and b
are always finite.
Also, I think that allowing infinite or nan values for a
and b
does have legitimate purposes. nan
is propagating which can be useful, and an Interval
with one of both of its bounds infinite is useful.
let a = 100..(Double.infinity)
let b = (Double.infinity)..200
let c = a.intersection(with: b) // 100.0 .. 200.0
Swift is a strongly typed language: the tuple type (Double, Double)
accomplishes exactly this. But to be clear, a tuple is not necessary: note how Apple's vectorized linear interpolation function API is designed, with the two bounds as separate arguments:
linearInterpolation(
vectorA,
vectorB,
using: interpolationConstant)
Your example can be spelled similarly:
linearInterpolation(1, 6, using: 0.2)
The point here, as I wrote above, is precisely that it's not necessary to encapsulate two floatingpoint values with a separate type for this function, as a == b
is perfectly ergonomic.
And there is nothing to stop you from continuing to use such a design for your own work. But I don't see how it could justify adding a second type to represent an interval in the standard library.
The question to figure out is whether there's some functionality in your type which can't be reasonably accomplished today with ClosedRange<T>
, or StrideThrough<T>
, or a tuple (T, T)
, or even just two separate values of type T
; if not, whether new APIs could be added that works with these types to make these use cases easier; and only if not then, perhaps consideration of a unique type.
Are you suggesting that, for instance, CGRect.isEmpty
is unnecessary because r.width == 0 && r.height == 0
is "perfectly ergonomic"?
I will do some experiments with tuples to try to achieve the same results I do with Interval
and post the result here for comparison.
In the general real number cases, if isEmpty
returns true
, then both isAscending
and isDescending
will return false
.
No. Though I would argue that r.width == r.height
doesn't require a separate API CGRect.isSquare
, this is entirely beside the point.
What I'm suggesting is that in your case, the two bounds shouldn't need to be encapsulated in a single value in the first place. In other words, if (a, b).0 == (a, b).1
seems unwieldy, the solution isn't to create a type that duplicates a tuple so that you can add a comparison property, it's to write a == b
.
Lest this seem like exaggeration, let me point out the example in the first post here:
Color(
r: t.interpolated(to: r..other.r),
g: t.interpolated(to: g..other.g),
b: t.interpolated(to: b..other.b)
)
An instance of the proposed type Interval
is created three times here, where each time it is used as an argument to call a function that immediately retrieves the bounds again from the encapsulating instance. Adopting Apple's API design, by contrast, would yield something like this:
Color(
linearInterpolation(r, other.r, using: t)
linearInterpolation(g, other.g, using: t)
linearInterpolation(b, other.b, using: t))
...requiring no other type but Color
.
Interval
is Equatable
so of course it does what tuples do in that regard. But if all I have is a tuple, t.0 == t.1
is my only option, which is not, from a callsite perspective, conducive to understanding the code. I could add the free function func isEmpty<T: FloatingPoint>(_ t: (T, T)) > Bool { t.0 == t.1 }
but this would also be more awkward at the call site then simply asking the Interval
directly, "do you subtend any space?" i.e., isEmpty
.
On the contrary! For the reasons discussed above, users would expect isEmpty
to be false
when the bounds are equal, and t.0 == t.1
is certainly the more unambiguous spelling. That it is your only option with a tuple is arguably a feature, not a bug!
So a few things I'm noticing while trying to implement your approach above:

import Accelerate
is required. Not conducive to learning the language, more conducive if performance is what is required. EDIT: Also,Accelerate
is not available on nonApple platforms, and is thus not "pure Swift".  There is no function called
linearInterpolation
. It is a static function calledvDSP.linearInterpolate
and only operates on inputs conforming toAccelerateBuffer
. The only types which conform areSlice
andUnsafeBufferPointer
. Arrays can conform, but individual scalars do not. So I have to switch to using arrays for my components.SIMD3<Double>
and similar types also do not conform. Also, tuples do not conform.
import Accelerate
struct Color : CustomStringConvertible {
let c: [Double]
init(_ c: [Double]) {
assert(c.count == 3)
self.c = c
}
init(r: Double, g: Double, b: Double) {
self.c = [r, g, b]
}
var r: Double { c[0] }
var g: Double { c[1] }
var b: Double { c[2] }
private func f(_ n: Double) > String { return String(format: "%.2f", n) }
var description: String { "(r: \(f(r)), g: \(f(g)), b: \(f(b)))" }
func interpolated(to other: Color, at t: Double) > Color {
return Color(vDSP.linearInterpolate(c, other.c, using: t))
}
}
While this might be a win from a performance view, it is a very bespoke approach compared to simply being able to use Interval
interpolation across a wide variety of common applications.
I do not expect CGRect.isEmpty
to return false
when it is in fact geometrically empty. I have stated that the purpose of Interval
is primarily to handle geometric cases, not settheoretic ones. The name of isEmpty
could be changed to isExtentEmpty
, which would avoid the possibility of confusion with the settheoretic case you're concerned about.
I'll also note that the solution just above is 24 lines of code, while my original one is 15.
I will also note that the Accelerate
framework is only available on Apple platforms, and is not "pure Swift," whereas my implementation is, and is therefore suitable for inclusion in Foundation.
A quick experiment on the geometric nature of isEmpty
:
import CoreGraphics
let r1 = CGRect(x: 50, y: 50, width: 0, height: 0)
r1.isEmpty // true
let r2 = CGRect(x: 0, y: 0, width: 100, height: 100)
r2.isEmpty // false
r2.contains(r1) // true
So it is possible for one geometric object to contain another that has position but subtends no space. The case simply degenerates to point inclusion. With Interval
the case degenerates to scalar inclusion:
import Interval
let i1 = 50..50
i1.isEmpty // true
let i2 = 0..100
i2.isEmpty // false
i2.contains(i1) // true
I'm not suggesting that you use the API as given (although you certainly can, as you show); I'm speaking of the API design itself. Good catch on the typo; it's indeed linearInterpolate
.
This would be inconsistent with the meaning of isEmpty
in the standard library; whatever Core Graphics does, floatingpoint values model the reals, and a closed interval is never empty.
As I said, if this proposal is taken seriously, I'd consider renaming the existing isEmpty
to isExtentEmpty
and adding an isEmpty
that conforms to the settheoretic notion. Is that satisfactory?
This is truly a stylistic consideration, but I like to be able to ask scalars to interpolate themselves between interval spaces, rather than rely on a family of free functions to accept a constrained family of types that do interpolation. For me (and I suspect many others) this is a natural way to think about interpolation, and results in code that is easy to understand and maintain. The Color example is but one of many I use Interval
for.
Pure Swift has no current idiom for general linear interpolation, and I think defining directed interval spaces and then being able to interpolate scalars between them is a good start.
I would find that confusing. "Extent" is just a synonym for "range" or "interval," and a closed interval is, unambiguously, not empty. The only clear expression of this, in my view, is to state what you mean: the bounds are equal, and the spelling for that is ==
.
It's fine to have a stylistic preference. The beauty of the language is that none of this requires compiler support, so you can vend your own package and share it with anyone who has the same preference as you do.
However, in that Swift has already evolved to merge range and interval types and abandoned or rejected the ..
operator, whatever idiom for linear interpolation is adopted in the standard library or a core library will need to fit with these decisions. That Apple has its own idiom in Accelerate
is a good precedent to look at.
I appreciate your critiques. The main thing I think I've realized is that Interval
is trying to do too much: it's trying to model a closed floating point interval (something that ClosedRange
already does) and it's trying to be a platform for linear interpolation, which is the aspect that is more interesting to me, and for which I don't see a standard idiom in pure Swift.
So I'm going to go back to the drawing board. I already have some ideas how to implement a more generic interpolation API. If you know of other such proposals/implementations, I'd appreciate a pointer.
Perhaps it's more accurate to name it isZeroLength
in this case? Or, maybe the type should named RealLineSegment
?