After seeing lots of discussion on the forums recently about what a general approach to protocol requirement synthesis might look like, I decided to explore a few ideas. The following is a (very early/rough) look at how I think such a feature might work. I'd be interested to hear any and all feedback about whether this seems like a good approach or general direction to take!
I will note that this pitch intentionally avoids relying on future language features like variadic generics which could potentially improve the ergonomics (but increase implementation complexity quite a bit).
Memberwise Requirement Synthesis
The goal of memberwise requirement synthesis is to synthesize a protocol requirement implementation for a type if all of its stored properties conform to the protocol. A protocol requirement can be marked as eligible for derivation by adding the @memberwiseDerivable attribute. The attribute takes three parameters: a mapper function, a reducer function, and an initial value for reduction. An example if this was applied to Equatable might look like this:
protocol Equatable2 {
@memberwiseDerivable(mapper: equatable2Mapper, reducer: equatable2Reducer, reduceInitialResult: true)
static func == (_: Self, _: Self) -> Bool
}
// lhs and rhs come from the derived requirement, memberKeyPath is added to the argument list
// the return type must match the reducer's argument types
// Self is replaced by generic parameter T, U is the type of the member (which conforms to the given protocol)
func equatable2Mapper<T, U: Equatable2>(_ lhs: T, _ rhs: T, memberKeyPath: KeyPath<T, U>) -> Bool {
return lhs[keyPath: memberKeyPath] == rhs[keyPath: memberKeyPath]
}
// the argument types must match the return type of the mapper
// the return type must match the return type of the derived requirement
func equatable2Reducer(_ first: Bool, _ second: Bool) -> Bool {
return first && second
}
Using the provided parameters, the compiler could then synthesize a conformance for a type which doesn't provide an implementation, which would look like this:
// Example memberwise conforming type
struct Compound: Equatable2 {
let a: A = A() // Conforming type
let a2: A = A() // Conforming type
let b: B = B() // Conforming type
// Compiler Synthesized requirement, not written by user
static func == (lhs: Compound, rhs: Compound) -> Bool {
return equatable2Reducer(equatable2Reducer(equatable2Reducer(true /* reduceInitialResult expression from attribute */,
equatable2Mapper(lhs, rhs, memberKeyPath: \Compound.a)),
equatable2Mapper(lhs, rhs, memberKeyPath: \Compound.a2)),
equatable2Mapper(lhs, rhs, memberKeyPath: \Compound.b))
}
}
I’ve uploaded a gist here with examples of how Equatable and Hashable requirements could be synthesized with this feature. It includes the code which would be inserted by both the user and the compiler.
Apologies for the long post, but I think its also worth answering a few of the questions I’m sure will come up:
Q: Why build the synthesized requirement from separate map and reduce steps?
A: I initially struggled to come up with a design which could preserve type information in the member key paths without relying on some form of variadic generics. Having a single output type from the map phase to pass off to the reduce phase sidesteps that issue. Also, most use cases for synthesized conformances that I’ve thought of so far fit well into the map-reduce model, because members are usually operated on one by one (This is true for Equatable/Hashable/AdditiveArithmetic/Comparable).
Q: Would this work for enums?
A: Not currently. In the future, this could probably be extended relatively naturally to cover enums if they gain KeyPath support
Q: How would this method compare to the current conformance synthesis built into the compiler performance-wise?
A: I don’t know yet . If the map/reduce functions could be inlined I think we’d be able to generate code roughly equivalent to the existing compiler synthesis, but it’s too early to say for sure.
Thanks for reading! I know this isn't very detailed yet for a pitch, but I thought it was worth starting the discussion around how we could make synthesized conformances a first-class feature in the language.