Skip to content

Commit 7768aa3

Browse files
authored
Merge pull request #81175 from kubamracek/embedded-docs10
[embedded] Add documentation for class-bound existentials in Embedded Swift
2 parents 6e33329 + b162447 commit 7768aa3

File tree

1 file changed

+108
-0
lines changed

1 file changed

+108
-0
lines changed

docs/EmbeddedSwift/Existentials.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Embedded Swift -- Existentials
2+
3+
**⚠️ Embedded Swift is experimental. This document might be out of date with latest development.**
4+
5+
**‼️ Use the latest downloadable 'Trunk Development' snapshot from swift.org to use Embedded Swift. Public releases of Swift do not yet support Embedded Swift.**
6+
7+
For an introduction and motivation into Embedded Swift, please see "[A Vision for Embedded Swift](https://github.com/swiftlang/swift-evolution/blob/main/visions/embedded-swift.md)", a Swift Evolution document highlighting the main goals and approaches.
8+
9+
## Background
10+
11+
Existentials (also known as "any" types) in Swift are a way to express a type-erased value, where the actual type is not known statically, and at runtime it can be any type that conforms to the specified protocol. Because the possible types can vary in size, the representation of such a value is an "existential container" and the actual represented value is stored either inline (when it fits) or indirectly as a pointer to a heap allocation. There are also multiple concrete representations of the existential container that are optimized for different constraints (e.g. for class-bound existentials, the value does not make sense to ever store inline, so the size of the container is matched to hold exactly one pointer).
12+
13+
Existentials are restricted in Embedded Swift in multiple ways, for multiple reasons:
14+
15+
- Value existentials are not allowed. This prevents the optimization barriers and heap allocation indirections that come with those existentials in regular Swift.
16+
- Class-bound protocols can be used as an existential. This still circumvents the often undesired behavior of existentials where they allocate (and deallocate) storage on the heap for the inner value if it cannot fit in the inline buffer, because class references are always refcounted and references are shared.
17+
- Unbounded generic methods cannot be called through an existential.
18+
19+
## Class-bound existentials
20+
21+
Embedded Swift allows and supports class-bound existentials:
22+
23+
```swift
24+
procotol ClassBoundProtocol: AnyObject { // ✅, this means any type that wants to conform to ClassBoundProtocol must be a class type
25+
func foo()
26+
}
27+
28+
class Base: ClassBoundProtocol { ... }
29+
class Derived: Base { ... } // also conforms to ClassBoundProtocol
30+
class Other: ClassBoundProtocol { ... }
31+
32+
let existential: any ClassBoundProtocol = ... //
33+
existential.foo() //
34+
```
35+
36+
Note that protocols that are not class-bound cannot form existentials (in Embedded Swift):
37+
38+
```swift
39+
let existential: any Equatable = ... //
40+
41+
class MyClass: Equatable { ... }
42+
let existential: any Equatable = MyClass // ❌, not enough that the actual type is a class, the protocol itself must be class-bound
43+
```
44+
45+
Class-bound existentials in Embedded Swift allow the "is" and "as!" / "as?" operators:
46+
47+
```swift
48+
let existential: any ClassBoundProtocol = ...
49+
if existential is Base { ... } //
50+
guard let concrete = existential as? Derived else { ... } //
51+
let concrete = existential as! Derived // ✅, and will trap at runtime if a different type is inside the existential
52+
```
53+
54+
## Restrictions on class-bound existentials
55+
56+
Class-bound existentials in Embedded Swift do come with some restrictions compared to class-bound existentials in regular Swift:
57+
58+
- You cannot use an existential to call a unbounded generic method from the protocol. This is described in depth in [Embedded Swift -- Non-final generic methods](NonFinalGenericMethods.md). For example:
59+
```swift
60+
protocol ClassBoundProtocol: AnyObject {
61+
func foo<T>(t: T)
62+
}
63+
64+
let ex: any ClassBoundProtocol = ... //
65+
ex.foo(t: 42) //
66+
```
67+
68+
- You cannot use an existential composition of a class-bound protocol with a non-class-bound protocol. For example:
69+
```swift
70+
let ex: any ClassBoundProtocol & OtherClassBound = ... //
71+
let ex: any ClassBoundProtocol & Equatable = ... //
72+
```
73+
74+
## Alternatives to existentials
75+
76+
When existentials are not possible (e.g. because you need struct types in an existential), or not desirable (e.g. because the indirection on a class-bound existential causes an observation performance degradation), consider one of the following alternatives (which all have different tradeoffs and code structure implications):
77+
78+
**(1) Avoid using an existential, use generics instead**
79+
80+
```swift
81+
protocol MyProtocol {
82+
func write<T>(t: T)
83+
}
84+
85+
func usingProtocolAsGeneric(p: some MyProtocol) {
86+
p.write(t: 42) //
87+
}
88+
```
89+
90+
**(2) If you only need a different type based on compile-time configuration (e.g. mocking for unit testing), use #if and typealiases:**
91+
```swift
92+
#if UNIT_TESTING
93+
typealias HWAccess = MMIOBasedHWAccess
94+
#else
95+
typealias HWAccess = MockHWAccess
96+
#endif
97+
98+
let v = HWAccess()
99+
```
100+
101+
**(3) If you only have a handful of tightly-coupled types that need to participate in an existential, use an enum instead:**
102+
```swift
103+
enum E {
104+
case type1(Type1)
105+
case type2(Type2)
106+
case type3(Type3)
107+
}
108+
```

0 commit comments

Comments
 (0)