`AdditiveArithmetic` conformance synthesis for structs

We've been experimenting with synthesizing conformances to AdditiveArithmetic (and other protocols) as part of differentiable programming in Swift, and we'd like to start pitching these through Swift Evolution.

AdditiveArithmetic generalizes types that define addition, subtraction, and a zero. Conforming types include both numeric scalar types, like Int and Float, as well as vector types like SIMD4<Double> (this is not implemented yet because of ABI concerns).

Similar to Equatable and Hashable synthesis, AdditiveArithmetic conformance synthesis for structs works when all stored properties conform to AdditiveArithmetic.

struct Point<T: AdditiveArithmetic>: @memberwise AdditiveArithmetic {
    var x, y: T

    // Compiler synthesizes:
    static var zero: Point {
        return Point(x: T.zero, y: T.zero)
    }

    static func + (lhs: Point, rhs: Point) -> Point {
        return Point(x: lhs.x + rhs.x, y: rhs.y + rhs.y)
    }

    static func - (lhs: Point, rhs: Point) -> Point {
        return Point(x: lhs.x - rhs.x, y: rhs.y - rhs.y)
    }

    static func += (lhs: inout Point, rhs: Point) {
        lhs.x += rhs.x
        lhs.y += rhs.y
    }

    static func -= (lhs: inout Point, rhs: Point) {
        lhs.x -= rhs.x
        lhs.y -= rhs.y
    }
}

var point = Point<Float>(x: 2, y: 3)
print(point + point) // Point<Float>(x: 4.0, y: 6.0)
print(Point<Float>.zero) // Point<Float>(x: 0.0, y: 0.0)

We believe AdditiveArithmetic conformance synthesis is important for numerical computing: it makes aggregates of numeric scalars or vectors just work with addition. Currently, it’s an important usability feature for differentiable programming in Swift.

Edit: As some have pointed out, memberwise derivation of AdditiveArithmetic doesn't make sense as a default implementation in all cases, unlike Equatable and Hashable synthesis. Thus, derivation is gated by the @memberwise attribute.

Feedback is welcome!

5 Likes

Unlike Equatable and Hashable, I suspect the synthesized conformance to AdditiveArithmetic would end up wrong fairly often.

struct Fraction : AdditiveArithmetic {
    var numerator: Int
    var denominator: Int
}
struct Movement : AdditiveArithemitc {
    var bearing: Double
    var distance: Double
}
12 Likes

I'm with @SDGGiesbrecht on this one. You named your example Point, but a Point shouldn't actually conform to AdditiveArithmetic anyway (it would be a Vector). The elaboration of that is that there are an awful lot of types where subtraction gives you a different type.

EDIT: More generally, default implementations should always be sensible and unsurprising, and I'm not sure that "memberwise addition" fits both of those adjectives.

3 Likes

This is essentially a user choice. If they declare a type to conform to AdditiveArithmetic with no custom implementations, they are declaring that this type is an additive group and thus requirements implementations are derived field-wise.

1 Like

I have to agree with @SDGGiesbrecht and @jrose here. Synthesized conformances especially (the synthesized default is not easily inspectable by the end user) but any default implementation generally should be predictable and appropriate for the overwhelming majority (if not all) cases.

(I suspect some degree of stretching this will be inevitable when ABI stability restrictions force any new additions to an existing protocol to have a default implementation, but that’s not at play here.)

A default implementation shouldn’t be simply one among many possibilities for what might be semantically correct. Otherwise, correctly conforming to a protocol becomes pervasively a two-step opt-in + opt-out process.


The calculus becomes different if, say, the protocol in question were a hypothetical FieldwiseAdditiveArithmetic. In other words, I think there would be broad agreement as to synthesized conformance if the semantics implied by protocol conformance were sufficient to guarantee the correctness of the synthesized default.

Or, put another way, a default implementation should be correct relying only on the semantics guaranteed by the protocol conformance itself, not on additional semantics to be implied by the absence of an overriding concrete implementation of the requirement.

2 Likes

How about adding an attribute or a keyword?

struct Foo: @deriving AdditiveArithmetic {
    var x, y: Int
}

The existing Equatable and Hashable derived conformances can also move to this model.

A keyword for derived conformances has already considered and explicitly rejected in favor of the current design. This allows us to think of synthesized conformances just like magical default implementations that with time could eventually be written in native Swift.

The problem here with what’s pitched has nothing to do with the syntax of synthesized conformances. The point is that AdditiveArithmetic as a protocol does not have semantics such that a default implementation of its requirements (magical or not) would be correct for conforming types. The objection is that conformance to a protocol + non-overriding of a default implementation cannot be used to imply additional semantics.

I agree with your analysis. However, the problem is making product space structs conform to a protocol using field-wise implementations. Requiring the user to manually implement every requirement is not practical, and type class derivation is a standard feature in many languages. I'm very interested in hearing what you think in terms of solving the actual problem to improve language expressivity.

Swift has this feature, both in the form of default implementations via protocol extensions, and in a few cases with synthesis on the compiler. The part that's missing is "field-wise"; when a field-wise implementation is not the {obvious, primary, least surprising} way to implement a protocol, it shouldn't be the default.

Xiaodi's suggestion of a dummy "FieldwiseAdditiveArithmetic" protocol that refines AdditiveArithmetic is one way around this problem. I don't love it because it's a one-off answer to what's probably a general concern, but it is a valid suggestion, and it's in line with how default implementations in the language work (extension Hashable where Self: RawRepresentable, for example).

1 Like

Ultimately we would be able to write custom Conformance Implementor in native Swift, but we'll likely need variadic generic first.

1 Like

Hmm, would adding a @fieldwise attribute solve the general concern? What's the concern with adding this attribute to specifically solve the non-{obvious, primary, least surprising} field-wise conformance derivations (which, say, will not include Hashable and Equatable because they are obvious, primary and least surprising)?

It solves that concern for me, yeah. Whether or not people feel like it generally fits with the direction of the language is different, but personally it seems like a case of "explicit opt-in" for something that (currently?) can only be done by the compiler.

Got it, thanks! Unless there is major pushback in this thread, @dan-zheng let's switch to pitching a @fieldwise attribute along with AdditiveArithmetic derivation instead.

Without commenting on this direction in general, I think @memberwise or @propertywise would be better colors for this bikeshed. They are more aligned with Swift’s existing terminology.

4 Likes

I agree about using the existing terminology. And @storedPropertywise would be more accurate and less surprising.

@memberwise is pretty good too, as it is already being used in the official language reference to refer to stored properties.

Memberwise Initializers for Structure Types

Structure types automatically receive a memberwise initializer if they don’t define any of their own custom initializers. Unlike a default initializer, the structure receives a memberwise initializer even if it has stored properties that don’t have default values.

The memberwise initializer is a shorthand way to initialize the member properties of new structure instances. Initial values for the properties of the new instance can be passed to the memberwise initializer by name.

3 Likes

@memberwise seems fine and may even potentially find applications elsewhere; I think @memberwise AdditiveArithmetic would be sufficiently clear.

7 Likes

I would want to see some other use cases before committing to @memberwise or similar. As mentioned, the most straightforward solution here, requiring no new attributes, would be a MemberwiseAdditiveArithmetic protocol that opts in to the synthesis.

Unless it's possible for @memberwise to remove the need for any special support for AdditiveArithmetic in the compiler? Would it be possible for @memberwise to work for a range of different protocols, with the compiler synthesising the “obvious” memberwise implementation if all the members conformed to a protocol? And what restrictions would that imply on the protocol?

1 Like

This works for AdditiveArithmetic because that type is essentially a group, and any direct product of groups is itself a group.

It won't work for any algebraic structure because e.g. the product of two fields is not itself a field (I think there is currently no abstraction in the Swift stdlib for fields, but it can of course be added manually). Of course, you can extend all the operations to the members in a straightforward way, but the thing will work differently than you expect it to. So I'm not sure it's really what you would want (basically, one of the problems is that the compiler can't verify certain laws that you would like to hold).

1 Like

Right. Any direct product of groups can be made into a group in the "obvious" way, but as several others have noted, the result is not necessarily the group that you actually want. Some examples:

  • fractions.
  • duration in hours and minutes (0:30 + 0:45 should be 1:15, not 0:75).

I.e. it's not necessarily what you want even for groups, so it's important to be explicit about it (@memberwise or similar); fields are a bit special in that it's never what you want--for most other algebraic structures it's at least sometimes correct, but as you noted, we don't have a field abstraction at present.

4 Likes

Makes sense. If there is not a reasonable number of plausible protocols where gated memberwise synthesis would make sense, and it seems like there probably isn't, then I struggle to see the point @memberwise here. Just make synthesis kick in for a MemberwiseAdditiveArithmetic protocol that inherits from AdditiveArithmetic and avoid the marker attribute that doesn't seem to generalise well.

Edit: And @memberwise is really just a niche version of @deriving or similar which was already suggested, and rejected, for opting in to compiler synthesis of Equatable, Hashable, etc.