Why does `Strideable` require `Stride` to conform to `SignedNumeric`?

i’ve got a Decimal<Unit> type i’m trying to implement a Strideable conformance for. it is its own Stride type, and provides the necessary addition and subtraction API. however, the Stride type has to conform to SignedNumeric, which in turn inherits from Numeric, which requires the multiplication operator *(_:_:).

this would be all fine and good, except the *(_:_:) protocol requirement requires the left-hand and right-hand-side parameters to be of type Self, which does not make sense for an arbitrary Decimal<Unit>. (it should only support multiplication where one of the factors has Unit == Void.)

let a:Decimal<Seconds>, 
    b:Decimal<Void>

let x:Decimal<Seconds> = a * b // okay
let y:Decimal<Seconds> = b * a // okay

let z:Decimal<Seconds> = a * a // this is clearly bogus...

why does Strideable require its Stride to support *(_:_:)?

Yes, the requirement is stricter than the semantics of Strideable require. As it is, a type can really only conform if its Stride is a dimensionless number.

There are two pragmatic reasons I can think of why the requirement is the way it is—

  1. AdditiveArithmetic now sits above Numeric in the hierarchy (it wasn’t available when Strideable was designed), but there is no corresponding protocol for “signedness” that sits above SignedNumeric, nor could one be added retroactively now. Additionally, it was intended that strides should be able to conform to Collection protocols (not yet possible because we’d need this to be in the form of distinct non-overlapping conditional conformances); O(1) random access is trivial when the stride type supports multiplication, but we’d then want a protocol refining AdditiveArithmetic for types that support scalar multiplication that we also don’t have.

  2. The actual implementation of striding is just barely possible with the requirements we do have. It’s actually been devilishly hard to ensure that it works both correctly and performantly even for integer Stride types due to trapping on overflow, for instance. (Floating-point stride is implemented separately using fused multiply-add when possible to behave as users expect.) Relaxing the requirements further, even if that could be done without breaking ABI, may risk making strides unimplementable without either sacrificing performance for the most common use case of stride(from: 0, to: 42, by: 2) or correctness pitfalls.

7 Likes

Context: everything about Stridable is slightly wonky, and (speaking personally) it's something that we would very much like to rework if we could, but it mostly works well enough and certainly isn't worth breaking ABI for.

Xiaodi's (1) is pretty much the answer to the question being asked; the numeric protocol hierarchy was pretty limited when Stridable was added, and here we are.

6 Likes