Static Protocols

Hello, Swift community!

I’d like to solicit some feedback about an idea that I’ve got.

Introduction

A notable part of SE-0302: Sendable and @Sendable closures is the formal concept of a marker protocol (declared with the new compiler-internal @_marker attribute) that can be applied to a protocol declaration, which imposes the following rules on that protocol:

  • It can’t have requirements of any kind.
  • It can’t refine a non-marker protocol.
  • It can’t be used as the second operand of is, as? and as! operators.
  • It can’t be used inside a generic type constraint expression, which is defining a conditional conformance to a non-marker protocol.

This is a very useful tool for defining completely compile-time protocols that are predominantly used by the compiler for enforcing special rules.

Inspired by this concept and recalling a very notable use case (see below), I’d like to pitch the idea of adding the concept of a static protocol.

Motivation

One very powerful design pattern that is used extensively within SwiftUI is what I’m calling “the static protocol pattern”, where a protocol is only used for its static members and the conforming types are never expected to have an instance and instead are used as a metatype instance.
Most notable examples are EnvironmentKey and PreferenceKey.

First-class support for static protocols would help streamline the usage of this design pattern and make it easier (especially for newcomers) to avoid confusion and misuse.

Detailed Design

A static protocol is defined like a regular protocol, but with the static keyword applied to it:

static protocol EnvironmentKey {
    associatedtype Value
    static var defaultValue: Value { get }
}

A static protocol is governed by these rules:

  • It can’t have requirements that explicitly or implicitly mention an instance of Self (e.g. non-static members, initializers, operators, etc…).
  • It can’t refine a non-static protocol.
  • It can’t be refined by a non-static protocol.
  • It can’t be used as the second operand of is, as? and as! operators except in the form of a metatype (e.g. something is MyStaticProtocol.Type.self).
  • It can’t be conformed to by an inhabitable type (for now, only enums with no cases can conform to a static protocol).
  • It can’t directly or indirectly become a type constraint for a non-metatype instance. For example, if it’s a constraint on type T, then there cannot exist an instance of type T (associatedtype T: Collection where Element: MyStaticProtocol is an error).
  • Conformance to a static protocol cannot be added to a type that may become inhabitable in an ABI-compatible manner. For example, a no-case enum will either have to be @final Or the conformance will have to be specified in the same module as the enum.

Future Directions

One notable future direction would be to add syntactic sugar to make working with static protocol metatypes easier, seeing as there can never be an instance of a static protocol.

6 Likes

Can you elaborate on this point more? More specifically, what advantages or new functionality could developers gain by defining a static protocol?

The advantage is in the fact that protocols are predominantly used to provide a set of functionality to a generic value (by defining a witness table for the value’s existential container), which the user is most likely to think of when seeing a protocol. Furthermore, the expectations about the usage of a static protocol don’t become apparent until the user deals with some APIs that make use of it in the way described by the pitch. And even then, there’s no apparent reason why types conforming to a protocol like EnvironmentKey shouldn’t have instances. The philosophy of swift stating that “it should be easy to do the right thing and should be hard to do the wrong thing” is violated, because the user has to go out of their way to discover the proper use of the protocol. So, without a compile-time enforceable static protocol feature, the likes of EnvironmentKey just aren’t able to express their purpose clearly. With this feature, the user will have a clear understanding of the purpose of a protocol and how to use it and the compiler would help steer the user in the right direction by disallowing use cases that are nonsensical for the protocol in question.

1 Like

Isn't this already true:

static protocol EnvironmentKey {
    associatedtype Value
    static var defaultValue: Self.Value { get }
}

Or did I misunderstand something here? :thinking:

I meant it cannot mention an instance of Self, but it can mention the metatype Self itself. For instance, it can easily access the associatedtype because that doesn’t have anything to do with an instance of Self, but it would emit an error if one would declare an operator requirement for the protocol.

This is very interesting Gor, thank you for pushing on this. I'm thrilled to see this get built out!

3 Likes

I’d love to go ahead and implement this, so that it can graduate from a pitch to a proposal, but I just don’t know where to start when it comes to the Swift compiler. I do know C and C++ very well, but there so so much information to be known before one can make any sort of educated decision on a feature implementation like this. I’ll try reading all the docs that I can in the swift compiler repository and if that doesn’t help me understand the swift compiler’s architecture and code base, I’ll have to ask for help on these forums. I haven’t done any compiler/programming-language development at all, I might be missing some core concepts.