Hello everyone,
The following is a pitch for metatype keypaths in the Swift language. We welcome any feedback or questions about this pitch!
[Pitch] Metatype Keypaths
- Proposal: SE-NNNN
- Authors: Amritpan Kaur, Pavel Yaskevich
- Review Manager: TBD
- Status: Awaiting Implementation
Introduction
Key path expressions access properties dynamically. They are declared with a concrete root type and one or more key path components that define a path to a resulting value via the type’s properties, subscripts, optional-chaining expressions, forced unwrapped expressions, or self. This proposal expands key path expression access to include static properties of a type, i.e., metatype keypaths.
Motivation
Metatype keypaths were briefly explored in the pitch for SE-0254 and the proposal later recommended them as a future direction. Allowing key path expressions to directly refer to static properties has also been discussed on the Swift Forums for database lookups when used in conjunction with @dynamicMemberLookup and as a way to avoid verbose hacks like referring to a static property through another computed property. Supporting metatype keypaths in the Swift language will address these challenges and improve language semantics.
Proposed solution
We propose to allow keypath expressions to define a reference to static properties. The following usage, which currently generates a compiler error, will be allowed as valid Swift code.
struct Bee {
static let name = "honeybee"
}
let kp = \Bee.Type.name
Detailed design
Metatype syntax
Keypath expressions where the first component refers to a static property will include .Type
on their root types stated in the key path contextual type or in the key path literal. For example:
struct Bee {
static let name = "honeybee"
}
let kpWithContextualType: KeyPath<Bee.Type, String> = \.name // key path contextual root type of Bee.Type
let kpWithLiteral = \Bee.Type.name // key path literal \Bee.Type
Attempting to write a metatype keypath without including .Type
will trigger a diagnostic with a fix-it that recommends adding .Type
:
let kpWithLiteral = \Bee.name // cannot refer to static member `name` on instance `Bee` without `.Type`.
Keypath expressions where the component referencing a static property is not the first component do not require .Type
:
struct Species {
static let isNative = true
}
struct Wasp {
var species: Species.Type {Species.self}
}
let kpSecondComponentIsStatic = \Wasp.species.isNative
Access semantics
Immutable static properties will form the read-only keypaths just like immutable instance properties.
struct Tip {
static let isIncluded = True
let isVoluntary = False
}
let kpStaticImmutable: KeyPath<Tip.Type, Bool> = \.isIncluded
let kpInstanceImmutable: KeyPath<Tip, Bool> = \.isVoluntary
However, unlike instance members, keypaths to mutable static properties will conform to ReferenceWritableKeyPath
because metatypes are reference types.
struct Tip {
static var total = 0
var flatRate = 20
}
let kpStaticMutable: ReferenceWriteableKeyPath<Tip.Type, Int> = \.total
let kpInstanceMutable: WriteableKeyPath<Tip, Int> = \.flatRate
Effect on source compatibility
This feature breaks source compatibility for key path expressions that reference static properties after subscript overloads. For example, the compiler cannot differentiate between subscript keypath components by return type in the following:
struct S {
static var count: Int { 42 }
}
struct Test {
subscript(x: Int) -> String { "" }
subscript(y: Int) -> S.Type { S.self }
}
let kpViaSubscript = \Test.[42] // fails to typecheck
This keypath does not specify a contextual type, without which the key path value type is unknown. To form a keypath to the metatype subscript and return an Int
, we can specify a contextual type with a value type of S.Type
and chain the metatype keypath:
let kpViaSubscript: KeyPath<Test, S.Type> = \Test.[42]
let kpAppended = kpViaSubscript.appending(path: \.count)
Implications on adoption
Metatype keypaths are not back-deployable.
Implementation of metatype keypaths requires changes to the KeyPath
type in the Swift standard library and a new runtime is necessary to take full advantage of this feature. Older compilers will continue to be supported but the correctness of operator comparisons (eg. Equatable
or Hashable
) will not be guaranteed for keypath references to static properties.
For example, Bank
is a library compiled with Swift 5.10 or older:
public struct Bank {
public static var checking: Double = 10.00
public static var savings: Double = 200.00
}
Lets use Bank
’s static properties in libraries compiled with the latest compiler that includes the metatype keypath implementation:
//--- UIManager.swift
import Bank
public let checkingUI = \Bank.Type.checking
public let savingsUI = \Bank.Type.savings
//--- StorageManager.swift
import Bank
public let checkingStorage = \Bank.Type.checking
public let savingsStorage = \Bank.Type.savings
When the above keypaths are compared elsewhere in the codebase, they may not reliably produce correct results unless the Bank
library is updated to support the metatype keypath feature:
print(checkingUI == checkingStorage) // prints false, should print true
print(checkingUI != savingsStorage) // prints false, should print true
To leverage metatype keypaths effectively, library and framework binaries will need to be recompiled with a compiler that includes this implementation.
Future directions
Key Paths to Enum cases
Adding language support for read-only key paths to enum cases has been widely discussed on the Swift Forums but has been left out of this proposal as this merits a separate discussion around syntax design and implementation concerns.
Since references to enum cases must be metatypes, extending keypath expressions to include references to metatypes will hopefully bring the Swift language closer to adopting keypaths to enum cases in a future pitch.
Acknowledgments
Thank you to Joe Groff for providing pivotal feedback on this pitch and its possible implementation and to Becca Royal-Gordon for an insightful discussion around the anticipated hurdles in implementing this feature.