TLDR: what's the most principled way to represent memberwise initializers as pseudo "protocol requirements", such that they work with derived conformances and don't clash with user-defined initializers?
Context: memberwise initializer synthesis is necessary for usable automatic differentiation (work done on the tensorflow
branch). There are a few compiler-known protocols (AdditiveArithmetic
[implemented in Swift 5 without derived conformances], VectorNumeric
, and Differentiable
) and derived conformances should work for a struct to one of these protocols when all stored properties conform to the protocol:
// Simple example.
// Note: stored property type `Float` conforms to `AdditiveArithmetic`.
struct Point : AdditiveArithmetic {
// Derived conformances should work when all stored properties
// conform to the desired protocol.
var x, y: Float
// All protocol requirements should be synthesized via derived conformances.
// This is critical for usability of these protocols.
// Synthesized code:
/*
static func +(lhs: Point, rhs: Point) -> Point {
// SEE HERE: Requires memberwise initializer!
return Point(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static var zero: Point {
// SEE HERE: Requires memberwise initializer!
return Point(x: Float.zero, y: Float.zero)
}
*/
}
Memberwise initializers must be identifiable by the compiler in order to support derived conformances to these protocols for aggregate types. This is critical for the usability of these protocols: users need only define stored properties and all requirements are synthesized. Synthesized implementations simply forward the implementations of their members.
Currently, memberwise-initializer-ness is simply identified via a bit (AbstractFuncDecl::isMemberwiseInitializer
); only compiler-synthesized initializers are marked as memberwise initializers, user-defined initializers never count.
Derived conformances implemented on the tensorflow
branch currently identify memberwise initializers by looking for a ConstructorDecl
member where isMemberwiseInitializer == true
, and synthesizing a memberwise initializer if one doesn't already exist. However, this alone is insufficient and leads to bugs when users define custom initializers with the same signature:
import TensorFlow
// Note: `Float` conforms to `Differentiable`.
public struct Foo: Differentiable {
public let bar: Float
public init(bar: Float) {
self.bar = bar
}
}
// Compiler synthesizes a duplicate memberwise initializer:
// invalid redeclaration clash.
test.swift:6:12: error: invalid redeclaration of 'init(bar:)'
public init(bar: Float) {
^
test.swift:4:15: note: 'init(bar:)' previously declared here
public struct Foo: Differentiable {
^
Main question: I wonder what's the most principled way to avoid this problem (and in general, to represent memberwise initializers as pseudo-protocol-requirements)?
The simplest solution is to look for user-defined initializers with the expected memberwise initializer signature, and to use them during derived conformances if they exist. We'll proceed with this as our solution for fixing "duplicate initializer" bugs like the one shown above.
However, the main tradeoff is that there's no guarantee that user-defined memberwise initializers actually perform memberwise initialization: they can contain arbitrary code, which may lead to unexpected semantics for derived conformances.
- For example,
Point.zero
from above wouldn't returnPoint(x: 0, y: 0)
if there's a user-defined initializerPoint.init(x: Float, y: Float)
that does something arbitrary likeself.x = 1337; self.y = 1337
. - On the other hand, users can already customize protocol requirements to have arbitrary behavior (e.g. hardcoding
Foo.==
to return false to mess up derived conformances for structs with aFoo
stored property). But since these are protocol requirements, they can come with semantic requirements for implementations. Memberwise initializers OTOH cannot be expressed as protocol requirements.
Some other ideas/considerations:
- One way to gain confidence that "memberwise initializers" do actually perform memberwise initialization is to use an obfuscated name to prevent confusion with user-defined initializers:
static func __memberwiseInitializer(x: X, ...) -> Self
. The tradeoff is that it's too ugly for users to call this directly (if it's a goal to expose memberwise initialization functionality to users). - It may be nice if memberwise initializers are public so that they can called from other modules.
- I don't believe this is necessary for automatic differentiation.
- This is not the current behavior of synthesized struct memberwise initializers, which have internal access.
- Looking to the future: this problem is about "representing memberwise initializers as requirements", not about synthesis of memberwise initializers, which is trivial and already exists. The addition of macros that enable iteration over struct members (so that derived conformances can be defined using macros) wouldn't help solve this problem. We would need something like SE-18: flexible memberwise initialization, which exposes a
memberwise
keyword in user syntax and could enablememberwise init
protocol requirements.
Any ideas/feedback would be appreciated!
cc @rxwei, @Alejandro who might be interested from his work on memberwise initializers