[swift unboxed]

Safely unboxing the Swift language & standard library


Conditional Conformance

Thinking through conditional conformance in Swift, and working through the basics of its implementation.

25 January 2018 ∙ Swift Language ∙ written by

What does it mean for something to be conditional? The thing is not certain, and depends on some other state or set of requirements.

What does conformance mean? In Swift, we say a named type (such as an enum or class) conforms to a protocol when it implements the protocol’s requirements.

Put them together and you get the magic of conditional conformance: conforming to a protocol, but only sometimes, based on some other condition.

What is it good for and how does it work?

Current Conditions

Let’s start with a simple wrapper type:

struct ValueWrapper<T> {
  let value: T
}

We want to get the string representation of the value inside. If T is String, this is easy:

extension ValueWrapper where T == String {
  var stringValue: String {
    return self.value
  }
}

And it’s easy to use!

let wrapped = ValueWrapper(value: "This is a string")
wrapped.stringValue // returns "This is a string"

What about integers, doubles, and all those other types? Even staying in the realm of strings, this doesn’t work for substrings:

let wrappedSubstring = ValueWrapper(value: "Substring".prefix(3))
wrappedSubstring.stringValue // ERROR! ❌
// 'ValueWrapper<String.SubSequence>' (aka 'ValueWrapper<Substring>')
// is not convertible to 'ValueWrapper<String>'

No problem, we think! Both strings and substrings conform to StringProtocol so we can tweak our extension a bit:

extension ValueWrapper where T : StringProtocol {
  var stringValue: String {
    return String(self.value)
  }
}

Now things work for both strings and substrings:

let wrappedSubstring = ValueWrapper(value: "Substring".prefix(3))
wrappedSubstring.stringValue // returns "Sub" ✅

We’ve successfully added an extension whose contents are conditional on the where clause.

This all works in Swift 4.0; you can read more about it in the Swift book section Extensions with a Generic Where Clause.

Even for a contrived example it’s a little silly — why not conform to CustomStringConvertible, which already encapsulates the notion of returning a string representation of a thing? 🤔

Conditional Conformance

To conform to the CustomStringConvertible protocol, you need to provide a read-only string property named description. Let’s transform the above extension to conform to the protocol and replace stringValue with description:

extension ValueWrapper : CustomStringConvertible where T : StringProtocol {
  var description: String {
    return String(self.value)
  }
}

We did it — this is conditional conformance! Our type ValueWrapper conforms to protocol CustomStringConvertible on the condition that its generic type T conforms to StringProtocol. 🎉

Unfortunately, this doesn’t work in Swift 4.0:

error: extension of type ‘ValueWrapper’ with constraints cannot have an inheritance clause

😭

Swift 4.0 State of the World

Where does this error come from? We can search the compiler source code for the error and see that it’s defined in DiagnosticsSema.def:

ERROR(extension_constrained_inheritance,none,
      "extension of type %0 with constraints cannot have an "
      "inheritance clause", (Type))

Then we can look for extension_constrained_inheritance, which we’ll find in TypeCheckDecl.cpp:

// Constrained extensions cannot have inheritance clauses.
if (!inheritedClause.empty() &&
    ext->getGenericParams() &&
    ext->getGenericParams()->hasTrailingWhereClause()) {
  diagnose(ext->getLoc(), diag::extension_constrained_inheritance,
           ext->getExtendedType())
  .highlight(SourceRange(inheritedClause.front().getSourceRange().Start,
                         inheritedClause.back().getSourceRange().End));
  ext->setInherited({ });
}

Note the conditions in the if clause that trigger an error:

  1. The inheritance clause is not empty. In our case, this is the conformance to CustomStringConvertible.
  2. A generic parameter exists. This is our T.
  3. Finally, the generic parameter has a where clause, which ours does to check that T conforms to StringProtocol.

All three conditions are true, thus we get an error in Swift 4.0.

Swift 4.1 State of the World

Let’s jump ahead to Swift 4.1. First off, the error message and error trigger mentioned above are gone — check out the diff on GitHub.

The feature itself is there, so our ValueWrapper type works as you’d expect:

let wrappedString = ValueWrapper(value: "This is a string")
print("Wrapped value: " + wrappedString.description)
// prints "Wrapped value: This is a string"

let wrappedInt = ValueWrapper(value: 42)
print("Wrapped int value: " + wrappedInt.description) // ERROR ❌
// Compile-time error:
// type 'Int' does not conform to protocol 'StringProtocol'

For the wrapped Int, we get a compile-time error. Something in the protocol conformance checker succeeds with a String but fails with an Int.

How can we find the code that does this? Again, let’s try using the error message to trace it back.

The Enormity of Conformity

The error message “type X does not conform to protocol Y” is defined as type_does_not_conform (again defined in DiagnosticsSema.def). If we search the code base for code that triggers that error, we come across TypeCheckProtocol.cpp, which has a promising filename.

There’s a section of code with the following comment:

// If we have a concrete conformance with conditional requirements that
// we need to check, do so now.

Promising!

At the end of the section is the result check:

switch (conditionalCheckResult) {
case RequirementCheckResult::Success:
  break;

case RequirementCheckResult::Failure:
case RequirementCheckResult::SubstitutionFailure:
  return None;

case RequirementCheckResult::UnsatisfiedDependency:
  llvm_unreachable("Not permissible here");
}

So conditionalCheckResult contains all the magic. How is that generated?

auto conditionalCheckResult =
  checkGenericArguments(DC, ComplainLoc, noteLoc, T,
              /*A*/     { lookupResult->getRequirement()
                            ->getProtocolSelfType() },
              /*B*/     lookupResult->getConditionalRequirements(),
                        [](SubstitutableType *dependentType) {
                          return Type(dependentType);
                        },
                        LookUpConformance(*this, DC),
                        /*unsatisfiedDependency=*/nullptr,
                        options);

I’m quickly getting out of my element here, so let’s wrap up and focus on two new arguments passed to checkGenericArguments:

  • A marks the list of generic parameters (T in our case)
  • B marks the list of conditional requirements (T : StringProtocol in our case)

Remember, checking whether type T implements protocol P isn’t something new — Swift has always been able to check this kind of thing.

The new part is where the check happens. Here, we see the call to checkGenericArguments() in the part of the code that determines whether our extension of ValueWrapper to conform to CustomStringConvertible should apply or not.

Conditional conformance

The Closing Brace

I have a better sense of what conditional conformance is but feel like I’ve only scratched the surface on how it works.

The benefits are clear, as you get nice changes like this in the standard library:

-extension Optional where Wrapped : Equatable {
+extension Optional : Equatable where Wrapped : Equatable { 

You can see the full diff, Make Optional, Array and Dictionary conditionally Equatable as well as the official blog post Conditional Conformance in the Standard Library for more standard library excitement.

For more prose reading, check out the original proposal SE-0143: Conditional conformances and the Generics Manifesto.

For more code reading, here are some diffs to start:

Have fun conforming to those protocols, conditionally or otherwise!

}