// I can use it for Result type.
let x: Result<Void, any Error> = .failure(AppError.unknown)
protocol ABC {}
struct ST: ABC {}
enum AppError: Error {
case unknown
}
struct Box<T: ABC> {
var value: T
}
let v: any ABC = ST()
// But I can't use it here.
// Type 'any ABC' cannot conform to 'ABC'
let b1: Box<any ABC> = Box(value: v)
This is a common pitfall, but now that we have existential types (i.e. the any
keyword) it is a little easier to explain.
The short answer would be that the types that conform to ABC
(for example the ST
struct) are different types than the type any ABC
.
You would need something like
extension any ABC: ABC {}
to ensure that the any ABC
type itself also conforms to the ABC
protocol (but that does not work, the compiler actually warns that "Extension of existential type 'any ABC' is not supported").
The Error
protocol is special in that the compiler actually does ensure that the any Error
existential type itself conforms to Error
, too. I believe there are few other such types, but I am not sure off the top of my head.
As a little addition to the short version above: the any ABC
type is basically a special type, a "Box" (unrelated to your type with the same name above) that has enough storage to hold "any" type that conforms to the protocol. If such a type is too big to be held in the storage, it puts its value on the heap and keeps a pointer to it, which is why using existentials can incur a performance hit (as it introduces one more level of indirection, including potential cost of memory cleanup and whatnot). "Can" as in it literally depends on which execution path your code goes through during runtime, some types that end up in your "boxes" might be small enough, other may not.
Here is the full list:
- @objc protocols that do not have static method or
init
requirements Sendable
andCopyable
AnyObject
- protocol compositions containing only 1-3
any Error
by itself
Thanks for the summary, the behavior was unclear to me.
I am curious about your thought on this feature. Is this a missing feature, design oversight or something which will be removed in the future?
No, it's just how the language works.
A protocol with associated types, static methods, or inits cannot conform to itself for type safety reasons. For example, if I have
protocol P {
init()
}
Then I can write this function:
func f<T: P>(_ t: T.Type) -> T {
return t.init()
}
It's totally fine to pass in a concrete type that conforms to P:
struct S: P {
init() {}
}
print(f(S.self))
However, imagine if any P
conformed to P. What would happen if I did this?
print(f((any P).self))
There are other reasons why we don't support general self-conformance, related to how values are represented in memory.
As @Gero said, it is best to think of any P
as a box for concrete types that conform to P
, and not as a "supertype" of all concrete types that conform to P.
The box is "big enough" to hold a concrete type, but you cannot "fit" one box inside of another box.
To be clear, in the general case, this isn't even how the language works—it's just how logic works.
For example, every type that conforms to FixedWidthInteger
is a fixed-width integer. Meanwhile, any FixedWidthInteger
must be able to hold a value of any conforming type (for example, an Int32
or an Int64
). This means that any FixedWidthInteger
is not a fixed-width integer. Therefore, it cannot conform to FixedWidthInteger
.
I tried to make a type Sendable
. it still doesn't work.
Usage looks like this:
struct Box<T: Sendable> { var value: T }
let b1 = Box<any Sendable>(value: ST())
Maybe only then it shouldn't compile (if there're init
/ static
requirements and whatever else causing a problem)?
FWIW this specific example works fine in other languages:
Same example in Kotlin
interface ABC {
fun foo()
}
class ST: ABC {
override fun foo() {
println("Hello, World!")
}
}
class Box<T: ABC>(val abc: T) {
fun foo() {
abc.foo()
}
}
fun main() {
val v: ABC = ST()
val b1: Box<ABC> = Box(v)
b1.foo()
}
Same example in C#
using System;
public interface ABC {
void foo();
}
public class ST: ABC {
public void foo() {
Console.WriteLine("Hello, World!");
}
}
public class Box<T> where T: ABC {
public T abc;
public Box(T value) {
abc = value;
}
public void foo() { abc.foo(); }
}
public class Program {
public static void Main() {
ABC v = new ST();
Box<ABC> b1 = new Box<ABC>(v);
b1.foo();
}
}
I didn't test what would they do in FixedWidthInteger
scenario.
My own understanding of Swift had been formed by the behavior of Error
.[1]
The Error
protocol is used extensively (Result
, Publisher
, ...) and from my own experience, allowing Error
to satisfy T: Error
requirement created the same false expectation in me (and many of my colleagues) as the OP had.
I understand the general idea that the platonic idea of a chair is not in fact a chair.
I was surprised that such exceptions exist ... It's not my goal to scrutinize the language design, I was just curious
Notes, resources ... so the thumbnails won't make the post 80 lines long :grimacing:
[1] It is no longer ... My attempt at past perfect .
I have reviewed:
https://www.youtube.com/watch?v=ctS8FzqcRug
swift/docs/ABI/TypeLayout.rst at main · swiftlang/swift · GitHub
I haven't reviewed:
swift/docs/Generics at main · swiftlang/swift · GitHub
Yeah, we could generate a meaningful “self-conformance” witness table to do double dispatch in this case, but there would be a code size cost.
This won’t work for class-constrained protocols though, because there the concrete type has to be a single reference counted pointer, but any P
is now a reference counted pointer together with a witness table - the existential is the wrong size.
In Java and C# an existential has the same runtime representation as the concrete reference type.
In fact those are the specific cases where self-conformance works in Swift; @objc protocols and Sendable etc don’t have a witness table.
Error is special in its own way, the existential has the same representation as an NSError instance and it stores the witness table inside the box.
I believe the Error special case mostly exists because Result predates typed throws.
A future direction is to allow declaring self-conformances in some way, like extension any P: P
, or even extension any P: Equatable
. Static methods and inits could then be given some kind of default implementation explicitly. There would also have to be some restrictions on usage of associated types in P, and P would have to be not class-constrained also.
Since the language currently seems to generate implementations for instance properties/methods on the existential for all protocols, could the need for a witness table be avoided by only allowing extension any P
where P: AnyObject
to define implementations for static/init protocol requirements? (of course, adding additional instance members that are not protocol requirements would be ok.) Then you’d need to set up a witness table to handle accessing those requirements from (any P).Type
but anything accessible from a value of type any P
would not need any changes from the status quo.
The problem preventing class-constrained protocols from self-conforming is lower level.
Imagine I have a function <T: P> (Array<T>) -> ()
and also that P: AnyObject
. Inside the function we assume that a value of T
is a single pointer, because of P: AnyObject
, so we also assume Array<T>
is just an array of pointers.
But any P
is a pair of pointers in this case because it stores the witness table as well as the reference; so Array<any P>
is an array of pairs.
You can’t call your function with T := any P
because Array<any P>
does not have the same layout as Array<T>
for any T.
You could however write your own type erased wrapper:
final class AnyP: P {
var wrapped: any P
init(_ wrapped: any P) {
self.wrapped = wrapped
}
// forward required methods
}
Yep. Somewhere in an alternate universe there is a Swift-like language without built-in existential types at all; instead the type-erased wrappers are generated by metaprogramming of some kind.
In C#, you would not be able to use the equivalent of FixedWidthInteger
as a type.
C# observes the invariant that any interface used as a type (i.e., existential) must satisfy itself as a constraint (i.e., conform to itself)—as Slava explained—and enforces this at the language level. (This does imply, unless I’m mistaken, that semantic guarantees not expressed in code cannot be enforced.)
Until later versions, C# did not support static abstracts in interfaces (i.e., static requirements without default implementation, such as FixedWidthInteger.bitWidth
). When they did, they had to adopt language changes so that interfaces with static abstracts could not be used as a type. As you might expect, users didn’t like having restrictions, but there really wasn’t a choice but to plug that hole.
See this C# proposal for a discussion of a spot they missed with their initial restriction that had to be fixed: Disallow interfaces with static virtual members as type arguments · Issue #5955 · dotnet/csharplang · GitHub
In Swift, we also used to have certain restrictions on which protocols could be used as existential types (the fondly remembered “Self or associatedtype requirements” diagnostic). We decided that it would be better to allow them to be used as types, but not expose the specific functionality that cannot be used, than to prohibit their use as types altogether.
As it's been explained by others, there are cases where, if the existential box of a protocol also conformed to the protocol itself, we would end up with nonsensical code.
There are cases though, for example the code you posted at the start of this, in which the conformance would be fine, so the compiler could allow this (as they do in some other languages) as an exception.
I'd say 2 things though:
- in case of future changes to the protocol (for example, adding an
init()
), one could end up with nonsensical code that shouldn't compile and that would require drastic changes to the whole architecture, so it might be better to just prevent it from the start (and I agree that it is better); - I'd strongly suggest to stop thinking about protocols as "interfaces", featured in many other languages, but not in Swift, and think of them instead as "type classes", also featured in many other languages, including Swift; this way the distinction between an existential box and a protocol requirement would become obvious.
In Swift (again, forget about Java-like, interface-based languages), this code
protocol Foo {}
struct Bar: Foo {}
struct Baz: Foo {}
doesn't mean "Bar
and Baz
are subtypes of Foo
": it means "Bar
and Baz
conform to the type requirements described by Foo
" (more concisely, "Bar
and Baz
are instances of Foo
").
If I write
func gimmeFoo<T: Foo>(value: T)
I'm not saying "you must pass a subtype of Foo
" (which is the case in Kotlin, for example, that for that reason also includes variance identifiers): I'm saying "you must pass a type that conforms to the requirements described by the Foo
protocol".
What about any Foo
? any Foo
is a concrete type (like Bar
and Baz
) that has some features and a certain interface. But there's no particular reason to think that the type any Foo
conforms to Foo
, implementing its type requirements.
For example, what if Foo
has an associatedtype
(a concept that doesn't exist in many other popular languages)? There are specific cases where it's possible to make it work in some sensible way, and Swift tries to do that, but we shouldn't expect it to work in the general case.
Error
is one of the special cases where the compiler specifically understands the protocol, so it can give superpowers to types related to it (also for convenience and usability). But it's a special, compiler-related case, and it shouldn't be compared to the general case.
I think that it's fair to say, that any additive change to a protocol
requirements might suffer the same consequences. I any case, additive change to a protocol
will in most cases cause compile-time error.
I have a different view on this issue. I freely admit my method of learning (in general) is naive however, as I've pointed above, having exception for Error
in particular, creates false expectations in people.
I understand the benefit of this nameless exception to the rule, but I think it that it could be beneficial, to have a (for example) Note box in the documentation, that Error is a special protocol satisfying any Error: Error
.
I think this is an example of standard library having an access to privileged capability and I respect that.
That said, I'm more satisfied with @Slava_Pestov's response A future direction is to allow declaring self-conformances in some way
which is in my opinion consistent with the goals stated above, compared to what I feel is your response, suggesting that this is how its going to be and we have to deal with it.
I like this phrasing a lot and will steal it when explaining Swift protocols to colleagues and students.
While a comparison with Java interfaces might be useful when explaining to a Java developer, there are certainly limits that we should point out.
Another way of putting it I have had success with myself in the past was "Bar
and Baz
have certain things in common, and we name the commonalities with the protocol Foo
". That also covered the case I often used for dependency injection of existing types (I am looking at you, Calendar
):
You define a protocol that a certain function of yours you want to inject an existing framework type into so that it contains all the functions and properties you use of that existing type. Then you conform the type to that (e.g. with extension Calendar: MyDateStuffProtocol {}
. Voila, you now can easily inject any type that conforms to whatever subset of functionality you use.
Yes, and that might be documented better indeed. To be fair, though, Swift in general has not too many of these trip-wires, and there's a lot of things newcomers might believe to be "special compiler magic" that is in fact just expressed normally with the language itself.
And to be more fair, I think @ExFalsoQuodlibet did not want to categorically forbid adding a way to add self-conformance to the language forever, I read that as them just pointing out we as a community probably want to be carefully considering the cost-benefit here. For now, I am also more on the careful side.
All in all, not all things that could be expressed in a language's are desperately required for it to make using it feel comfortable. As that one meme showed, in Latin, for example, you can distinguish between "fighting with somebody" as in struggling against them as an opponent and "fighting with somebody" as in using them as a weapon in a fight. English and German don't have that and so far, I don't miss it... (then again, I am not exactly a fighter...)