More Expressive Protocol Constraint Syntax

Swift 5 allows protocols to "constrain their conforming types to those that subclass a given class". This is implemented as (taken directly from the release notes)-

protocol MyView: UIView { /*...*/ }

protocol MyView where Self: UIView { /*...*/ }

Given that the : character is generally used to indicate either inheritance or conformance in protocol/class/struct definitions, reading the above in my opinion is jarring for both a beginner and an experienced Swift user. I was curious behind the rationale of this decision (I couldn't find the relevant evolution proposal), given that one of the primary tenets of Swift is expressive code. This is in direct conflict when you can have valid definitions such as-

protocol Mock: SIMD, NSFetchRequest<NSNumber> where MaskStorage == SIMD4<Int> { }

Background

I was recently writing a series of posts related to RealityKit when I noticed the following interface description for HasHierarchy-

protocol HasHierarchy: Entity {
    /// ...
}

Based on my experience with RealityKit, I knew that Entity was a class, which instinctively made me infer the above as "protocol HasHierarchy inherits from Entity", which is semantically incorrect in Swift. If I had no prior knowledge of Entity, I would instead deduce that Entity is a protocol and HasHierarchy inherits from it (given this is the "general" pattern).

Discussion

I understand that the : character is contextual, and has different meanings based on where it is used (function/property definitions, ternary operator, etc). However, when the context is defined, it usually has servers a singular purpose (correct me if I'm wrong). For instance, let myProperty: Int has an unambiguous meaning.

However, this is not the case for class/enum/struct/protocol definition, as the reader needs to have prior knowledge of the objects succeeding :. From the example at the top, the reader needs to be aware that SIMD is a protocol and NSFetchRequest is a class to disambiguate the definition (I acknowledge this was a contrived example, but the point still stands).

1 Like

What syntax do you propose?

1 Like

Ideally, something that makes : unambiguous in the specified context. From my understanding, it is currently used to-

  • Denote inheritance (class Subclass: Superclass)
  • Signify protocol conformance (struct Mock: MockProtocol)
  • Constrain protocol to subclasses of specified type (protocol SomeProtocol: SomeClass)

A trivial (and admittedly naive) solution would be to use 3 different identifiers for each of the above. At the expense of not polluting the language with new special characters, one alternative is to reuse characters that are "special" in a different context-

  • Let inheritance remain the purview of : (class Subclass: Superclass)
  • Conformance denoted by <- (struct Mock <- MockProtocol)
  • Protocol subclass constraint specified by || (protocol SomeProtocol |SomeClass|)

The above choice of characters do not have a deeper rationale other than separating the responsibilities for each of the concepts to different characters. But it does help explore how an alternative definition looks like.

The contrived example in the original post would then be-
protocol Mock <- SIMD |NSFetchRequest<NSNumber>| where MaskStorage == SIMD4<Int> { }
While still verbose, it is no longer ambiguous what each component from the above signifies and also requires no prior knowledge of any of the inputs.

Does it help your understanding to think of : as denoting a subtype relationship?

For better or worse, Swift requires you to know the kind of types in order to fully reason about the meaning of syntax. For example, foo.bar() is clearly a method call, but in order to understand whether it is statically or dynamically dispatched you need to know the following:

  • The static type of foo
  • Whether that static type is a generic type parameter
  • If not, whether that static type is a protocol, a class, or a struct
  • If a protocol, whether bar is a protocol requirement or an extension method

Comparatively, deducing the proper meaning of : seems simpler to me.

2 Likes

Perhaps not exactly a subtype, but upon reflecting on your comment, it does help to think of : as "related to", with the specifics determined by the exact type.

I like your example, but .bar() is essentially still a function call, the specifics of which are (as you listed) ambiguous. However, : can refer to both subtyping and inheritance, which are distinct concepts. This in my opinion is akin to .bar() being a function call if it's a struct and a property setter if it's a protocol (as an example).

: in this context does establish a subtype relationship. In all of the cases you've mentioned, if you write X: Foo, it means X is a Foo.

It doesn't matter whether X is a class, struct, enum, actor, or protocol, and it doesn't matter if Foo is a class or protocol. There is always that "is a" relationship.

For class declarations, inheritance must be specified before protocol conformances, so at most you need to know whether the first element in the list is a parent class or protocol. It seems that protocol declarations do not have the same rule, and allow subclass constraints anywhere in the list.

class SomeClass {}

// Error: superclass 'SomeClass' must appear first in the inheritance clause
class AnotherClass: CustomStringConvertible, SomeClass {
  var description: String { "Test" }
}

// OK for protocols, though.
protocol Foo: CustomStringConvertible, SomeClass {}

I agree that this is strange, but aligning those rules would be source-breaking. Perhaps it could be considered for Swift 6.

3 Likes