Hello,
I'd like to test an idea: the introduction of a new keyword in front of declarations: conformance
.
EDIT - the pitch is now located at: Explicit Protocol Fulfillment.
The original post is below:
The conformance
keyword means: "this declaration fulfills a protocol requirement". Its presence is never required. However, when present, the program is ill-formed unless the declaration does match a protocol requirement.
For example:
protocol P {
func foo()
}
struct S1: P {
func foo() { ... } // OK
}
struct S3: P {
conformance func foo() { ... } // OK
}
struct S4: P {
// error: function fooo() does not fulfill any protocol requirement.
conformance func fooo() { ... }
}
Motivation
Unlike class overrides that must be prefixed with the override
keyword, declarations that fulfill protocol conformances do not need any specific decoration. This creates a few problems for programmers.
The first problem appears when the rules of type inference miss the protocol requirement.
For example, this code compiles, but can miss the programmer's intent:
protocol Measurable {
var length: Double { get }
}
extension Measurable {
var length: Double { 0 }
}
struct Meter: Measurable {
// Misses protocol requirement because 'Int' is not 'Double'
let length = 1
}
// Prints 0.0 instead of 1.0
func test(_ p: some Measurable) { print(p.x) }
test(Meter())
The second problem appears when protocols evolve in time, and change their requirements. For example, this can happen with libraries when they bump their major version and introduce breaking changes. When the user upgrades the library, the compiler won't provide any help for updating methods and removing the ones that have become useless:
// MyLib
protocol Program {
- func crash()
- func run()
+ func run(arguments: [String])
}
extension Program {
- func run() { }
+ func run(arguments: [String]) { }
}
import MyLib
struct HelloWorld: Program {
// Dead code
func crash() { fatalError("Bye") }
// Needs to be updated
func run() { print("Hello World!") }
}
Note that in both examples above, the actual intent of the programmer can not be determined from looking at the code alone. It is impossible to tell if length
and run()
are intended to fulfill protocol requirements, or just happen to fulfill protocol requirements. This explains why it is impossible to address those problems in the current state of the language.
Yet both problems do exist, for programmers whose intent is to fulfill protocol requirements.
Proposed Solution
We propose that the intent to fulfill a protocol requirement can be made visible in the program, with the conformance
keyword.
The conformance
keyword would create a compiler error in our two examples:
struct Meter: Measurable {
// error: property 'length' does not fulfill any protocol requirement.
// note: protocol Measurable requires property 'length' with type 'Double'
// note: candidate has non-matching type 'Int'
conformance let length = 1
}
struct HelloWorld: Program {
// error: function 'crash()' does not fulfill any protocol requirement.
conformance func crash() { fatalError("Bye") }
// error: function 'run()' does not fulfill any protocol requirement.
conformance func run() { print("Hello World!") }
}
Those compiler errors help the user apply the proper fixes (solve type inference ambiguities, remove obsolete functions, and update the modified requirements):
struct Meter: Measurable {
conformance let length: Double = 1
}
struct HelloWorld: Program {
conformance func run(arguments: [String]) { print("Hello World!") }
}
Detailed Design
The conformance
keyword has the compiler search for a matching declaration in all statically known conformances at the location it is used. If no such declaration exists, the compiler emits an error.
Here are a few examples:
protocol P1 { func foo() }
// =====
struct S1: P1 {
// OK: matches P1.foo()
conformance func foo()
}
// =====
struct S2 {
// OK: matches P1.foo()
conformance func foo()
}
extension S2: P1 { }
// =====
struct S3 { }
extension S3: P1 { }
extension S3 {
// OK: matches P1.foo()
conformance func foo()
}
// =====
protocol P2: P1 { }
struct S4: P2 {
// OK: matches P1.foo()
conformance func foo()
}
// =====
protocol P3: P1 { }
extension P3 {
// OK: matches P1.foo()
conformance func foo() { ... }
}
// =====
protocol P4 { }
extension P4 {
// error: function 'foo()' does not fulfill any protocol requirement.
conformance func foo() { ... }
}
Known Limits
There are situations where the conformance
keyword won't be able to fully help the programmer.
-
When a type conforms to two protocols that declare the same requirement, the
conformance
keyword does not tell which conformance, or both, are intended. -
In some valid programs, the method that fulfills the conformance is not declared at a location where the compiler has enough static information to accept the
conformance
keyword. In the code below, we can't use the 'conformance' keyword here, because whenQ.foo
is compiled, theP
protocol is not statically known to be conformed to.protocol P { func foo() } protocol Q { } extension Q { func foo() { } } // <- struct T: P, Q { }
Despite those limitations, the conformance
keyword brings more safety to the language, and more programmer confidence in the correctness of the program.
Future Directions
-
When a type conforms to several protocols that declare the same requirement, the
conformance
keyword could be extended with the name of the protocol:protocol P { func foo() } protocol Q { func foo() } protocol R { func foo() } struct T: P, Q, R { // Only P and Q fulfillments were intended. // R fulfillment is fortuitous. conformance(P, Q) func foo() { ... } }
-
The
conformance
keyword could pave the way for a public version of@_implements(ProtocolName, Requirement)
:struct OddCollection: Collection { conformance(Collection.count) var numberOfElements: Int }
-
The
conformance
keyword could be made to accept statically invisible requirements, by using an explicit protocol name:protocol P { func foo() } protocol Q { } extension Q { conformance func foo() { } // compiler error conformance(P) func foo() { } // OK }
-
The
conformance
keyword could be made able to "auto-fix" some near misses:protocol Measurable { var length: Double { get } } extension Measurable { var length: Double { 0 } } struct Meter: Measurable { // OK: inferred as 'Double' instead of 'Int', due to the protocol requirement. conformance let length = 1 }
Alternatives Considered
-
@conformance
instead ofconformance
.Since
override
is a keyword, and fulfills a similar purpose,conformance
was preferred. -
Requiring
conformance
, juste like we requireoverride
.- This would break backward compatibility.
- Compiler performance. The conformance check has a cost.
- This would force programmers to use it on fortuitous fulfillments, ruining the very goal of the keyword which is to declare an intent.
- There are valid programs where the fulfillment has to be added at locations where the target protocol is not statically known.
- Adding a requirement to a protocol would create an opportunity for fortuitous fulfilment, and become a breaking change. Note: I'm not quite sure if adding a protocol requirement is already considered as a breaking change today or not.
-
Requiring the name of the fulfilled protocol.
This possibility is described in the future directions. But the feature already brings a lot of value, even without any explicit protocol name. After all,
override
does not tell which superclass is overridden.