Floating Point Interval

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.

2 Likes

I am contrasting to a loss in performance by performing construction-time 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 floating-point 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.

3 Likes

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 call-site 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!

1 Like

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 non-Apple platforms, and is thus not "pure Swift".
  • There is no function called linearInterpolation. It is a static function called vDSP.linearInterpolate and only operates on inputs conforming to AccelerateBuffer. The only types which conform are Slice and UnsafeBufferPointer. 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.

2 Likes

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 set-theoretic ones. The name of isEmpty could be changed to isExtentEmpty, which would avoid the possibility of confusion with the set-theoretic case you're concerned about.

1 Like

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, floating-point 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 set-theoretic 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.

1 Like

Perhaps it's more accurate to name it isZeroLength in this case? Or, maybe the type should named RealLineSegment?