Problems with Numeric Protocols

Swift protocols are at their most useful when they are extremely simple. Describing only one sort of requirement maximizes the number of types that can conform, and allows generic algorithms to be used more effectively.

An excellent example of this is the Sequence protocol and its refinements: each successive protocol adds a fairly simple requirement, and enables a wealth of optimizations and generic algorithms in doing so.

But then we get to the number protocols: AdditiveArithmetic and its refinements. And I think most agree that they are less enjoyable to work with.

AdditiveArithmetic itself is excellent, but I believe Numeric is where it starts to go downhill. It requires ExpressibleByIntegerLiteral for no apparent reason, and it’s horribly named: it should really be called MultiplicativeArithmetic. Whether it should inherit AdditiveArithmetic is a complicated question, but ultimately it’s probably fine.

SignedNumeric is immensely problematic, though: it is trivial to devise signed values that should not be multiplied (particularly when the values are meant to have associated units, like a duration), and this protocol is unusable for them.

Similar problems trickle down the entire protocol hierarchy. BinaryFloatingPoint inherits ExpressibleByFloatLiteral, even though the language only has decimal and hexadecimal floating point literals. You cannot specify an unsigned number without conforming to BinaryInteger specifically. The list goes on.

While I understand that usability is important, I feel that this could have been accomplished more easily using protocol composition and mathematical definitions. I think a lot of the protocols could also benefit from future support for “generic protocols” akin to existing generic structures, particularly when it comes to requiring a specific radix.

1 Like

Essentially all of these points were discussed at length during the original Evolution discussions. I'd highly recommend reviewing those conversations for the answers to your points, and in particular the deliberate reasons why the hierarchy is not based strictly on "mathematical definitions" and what the semantic requirements of these protocols are (number are dimensionless, by the way).

It is an entirely unproductive framing to drop a message that the topic of extensive deliberation--which has concluded--has "horrible," "immensely problematic" choices for "no apparent reason" with blanket statements about the way it should be that even a cursory study of the prior discussions would have answered.

If you have specific questions about how to make productive use of Swift's numeric protocols, feel free to bring up the concrete problems you're trying to solve in "Using Swift." A reminder that this is a working forum about discussion of ideas for the future evolution of the language; there are plenty of places elsewhere on the internet to share your hot takes. If you feel strongly that Swift's approach is the wrong way to design numeric protocols maybe get yourself involved in the .NET process where they're currently proposing the creation of a similar hierarchy inspired by Swift's example.

6 Likes

I read your entire series of articles on the matter, and I don’t think any of these concerns were satisfactorily addressed by them. Is there a reason protocol composition wasn’t used? Why are the ExpressibleBy protocols required anywhere?

A lot of the protocols feel like they should be composed of less specific protocols that do not in fact exist.

I'm not referring to my series of articles, which are not aimed at a full accounting of design choices, and which I pointedly did not bring up. As I said, the answers to your questions are readily available on these forums and I have full confidence that you are as capable as I am of using the search function to find them if you are so inclined.

1 Like

You mean this? That’s about the only relevant thing I can find when searching for “ExpressibleByInteger”.

Again, I can’t help but contrast this with the Sequence protocols, which all have fairly specific requirements that enable a wealth of generic algorithms. None of which mandate literal support.

Perhaps you are overestimating my ability to find things. Could you point me to an explanation for that requirement? What about a reason that there's a static property for the additive identity in AdditiveArithmetic but not for the multiplicative identity in Numeric?

iirc there's a brief summary of answers to those questions in SE-0233's Alternatives Considered, which spawns from that pitch thread you linked.

3 Likes
  1. On top of AdditiveArithmetic , add a MultiplicativeArithmetic protocol that refines AdditiveArithmetic , and make Numeric refine MultiplicativeArithmetic . This would be a natural extension to AdditiveArithmetic , but the practical benefit of this is unclear.

First off, an obvious practical benefit is consistency: as it says, that’s the natural extension of AdditiveArithmetic.

Second, if you wait for practical applications all the time, you’ll only ever be disappointed when one emerges. Mathematicians have been devising unique ways of applying these rules for centuries, and some of them might want to use Swift.

Long story short: Strideable has an associated type called Stride, which is used for representing a bidirectional offset. It needs to conform to SignedNumeric, but does not need to be multipliable.

It should require conformance to AdditiveArithmetic and Signed (or whatever you want to call a protocol that needs an additive inverse), but that second protocol doesn’t exist. And as a result, Strideable can’t be used for something it’d be otherwise perfect for.

AdditiveArithemtic infers the type is a field (or finite field). Duration and other such unit laden types buck the concept of the ABC conjecture since they don’t adhere to the multiplicative inverse rule. So from a pure maths perspective it is consistent, just not in a practical sense. I think if we could, perhaps that restriction should be re-evaluated and made to be more practical (perhaps with a more robust counterpart that enforces the conjectures)

2 Likes

What?

AdditiveArithmetic does not model a field. It does not even model a group, since it doesn’t require inverses, and doesn’t include multiplication either.

Even Numeric only models a ring, not a field, because it does not require a multiplicative identity (nor multiplicative inverses).

The protocol for fields is called AlgebraicField, and it is located in the Swift Numerics package.

I think they meant it is being treated like that, even if it shouldn’t be. I really feel like the terms of art should at least be in the documentation for these protocols, by the way.

Personally, I think there should be numeric protocols describing mathematical operations (not to be confused with operators), with precise documentation describing those operations (to ease conformance) and static properties describing things like identity elements (which means a generic algorithm doesn’t need a way of initializing said elements).

From there, you can compose rings, fields, and other algebraic structures.

1 Like

what exactly do you propose, just removal of "ExpressibleBy" conformances leaving the other bits as they are or something more radical?

I’ll ignore source compatibility for now, though even a major release of Swift obviously needs to care about it.

AdditiveArithmetic is mostly fine, but the documentation should actually specify addition’s properties (being commutative and associative, for instance). It should not require subtraction. It should require a static property called additiveIdentity, which may or may not be 0 depending on the type.

There should be a distinct protocol for types whose values have an additive inverse. Due to the definition of an additive inverse, this must inherit AdditiveArithmetic.

Subtraction should be a distinct protocol, with a default implementation provided for types that conform to AdditiveArithmetic and the additive inverse protocol. Due to that default implementation, this protocol would rarely need to be used directly. It should separately require the same additiveIdentity, or perhaps inherit AdditiveArithmetic directly. I’m not sure about the exact mathematical definition.

Multiplication is more interesting. Multiplication of real numbers is commutative, matrix multiplication is not, etcetera. Those two examples are, in fact, different operations entirely, and should be described using distinct protocols as a result (not all of which need be in the standard library). The relevant identities (which are likely distinct) should also be required as static properties.

You probably see where I’m going with this. Required operations, required initializers, and required representations should all be distinct.

1 Like

How useful would these changes be? I don’t think many programs would benefit form these changes. Adding new protocols would also complicate the creation of generic code.

The name of AdditiveArithmetic isn’t the only place where addition and subtraction are conflated: the name of the AdditionPrecedence precedence group also conflates addition and subtraction. I think it’s fine for addition to be conflated with subtraction within the standard library, since there isn’t really a better word for “both addition and subtraction”.

I don’t think we need to change the standard library — the proposed protocols should instead be defined in a package (or a library or a framework).

1 Like

Making new types conform to these protocols is much easier when they are simpler. And I’ve already given an example of a problem that these changes would solve.

As for AdditionPrecedence, you are conflating an operation and an operator. The operators + and - have the same precedence, so they do in fact have the same precedence group.

If you want an example of a type that could conform to AdditiveArithmetic but not the subtraction protocol, consider a SortedArray.