First of all congratulations on the proposal acceptance! I think this is a great step forwards for Swift as a language.
Now let me please try to clear up some of the above confusion about opaque types vs. existentials.
Using "some" with "any" would be confusing because "some Foo" is a placeholder for static type information known at compile-time, whereas "any Foo" would be a placeholder for dynamic type information only known at runtime.
Since Swift is a statically-typed language, and its type abstractions always refer to statically-known type information. We should keep it that way.
For details see below. (Warning, it's long.)
Definitions
- existential container type (noun): the statically-known, but invisible to the programmer, type of the value/object that contains an instance of a value/object of a non-statically-known type that conforms to the protocol associated with the existential container. AKA "protocol-type". An instance of this would be a "protocol-type object".
- existential type (noun): the dynamically-known type of the value/object contained within an existential container. This is NOT the type of the protocol-type object, it's the type of what's inside it. This type can only be determined via type-casting,
is
, or calling type(of:)
at runtime. (Protip: In some cases, the protocol-type object must first be cast to Any
to "open the container" before the dynamic existential type can be accessed.)
- existential (noun): this is used in confusing ways. When referring to something at runtime, "an existential" could be used to refer to a specific value or object whose static type is a protocol-type. When referring to something at compile-time, it usually refers to the existential container type itself.
- opaque type (noun): the statically-known, but invisible to the programmer or consuming code, return type of a function or property type of a variable. This type is not a container of some typeâit is that type. In a static sense, the opaque type is merely a placeholder to be replaced by a specific type at compile-time. An opaque type is a protocol name prefixed by "some".
- generic type (noun): the statically-known, visible to the programmer, parameter type or return type of a function, or property type of a variable. This acts very similarly to an opaque type except that consuming code will know exactly what the type is, at compile time.
- associated type (noun): a placeholder in a protocol for a specific type that must be declared (or be able to be inferred) on any type that conforms to the protocol; very often, this requirement can be satisfied by a generic type parameter via a typealias. In current Swift, an associated type can never be opaque but it can be a self-conforming existential.
- self-conforming (adjective): describes an existential container type that is statically known to conform to the same protocol that's also conformed to by the dynamic, existential type of its contents. A value/object of a self-conforming protocol-type will forward all member access to its contents.
- static (adjective): when referring to type information, it means type information that's known at compile time. When referring to a protocol requirement, it means a requirement that specifies a
static var
, static func
, or init
method.
- dynamic (adjective): when referring to type information, it means type information that's known only at runtime. This can be determined with type-casting (
as
/is
), reflection, or functions like type(of:)
. When used as a declaration modifier, e.g. dynamic var foo
it grants some Obj. C runtime features to that property.
- protocol requirements (noun): in Swift, a protocol is just a set of requirements. Every
func
, var
, and associatedtype
in a protocol, is a requirement. For static protocol requirement, see the definition of static
above.
- protocol-type (noun): see existential container type
Swift is a Statically-Typed Language
When reading a novel, it's confusing if the character whose point-of-view the story is being told from, keeps randomly changing every other sentence, or if we keep jumping around from a past-tense to a future-tense, etc.
To avoid this problem, Swift tells the broad strokes of its story entirely from the static point-of-view. As a statically-typed language, Swift keeps its explicit, declarative syntax abstractions squarely in the static perspective. Its declarative abstractions are all placeholders for static type information that will be determined at compile-time: generics, opaque types, associated types, protocol-types, etc.
Meanwhile, Swift handles dynamic type information in a totally imperative manner, always using dynamic type-casting like as
, is
, or functions like type(of:)
to make it clear which type information is dynamically determinedâthat which requires some lines of code to actually run before it will be accessible in code.
This is why the future direction of "any" just does not work for Swift, because it would make a declaration func foo() -> any Bar
sound like "any Bar" is a placeholder for static type information, which it simply isn't.
Opaque Types can be Existential Protocol-Types
Incorrect. As explained above in the thread, an opaque type can be an existential container type as long as the protocol is one of the three kinds of protocols in Swift whose protocol-type objects can self-conform:
@objc
protocols that don't have any static or initializer requirements
- the new
@_marker
protocols
- compiler-known protocols (such as
Swift.Error
âthere's a short list of protocols built into Swift that are allowed to self-conform despite not fitting any of the above categories, because they have special witness tables created in advance to facilitate self-conformance)
See this part of the Swift compiler code, thanks to @anthonylatsis for linking it above.
Existential container types are known at compile time so they can indeed qualify as "some"/opaque type that conforms to their own protocolâas long as the compiler agrees they can conform to their own protocol (self-conformance).
However the existential type of the value/object contained inside an instance of a given existential container type is not known at compile time. Whether or not a given type is an existential container type is irrelevant to the question of whether it can conform to its own protocol and therefore be used with generics/opaque types.
Swift's Static vs. Dynamic Type Information Is Confusing
The fact that opaque types and existential container types seem related in the minds of people who don't understand them is not a bad reason to clarify the language, but we should be very careful that whatever changes we make don't further confuse the issue about what type information is dynamically known vs. what type info is statically known.
Ideally we can add syntax that stays true to declarative = static, imperative = dynamic distinction that Swift has.
However the proposed future direction for syntax "any" would make an existential seem like a placeholder for static type info, as if the compiler has been upgraded to where now I can do:
protocol Bar {
associatedtype X
static var x: X
}
func bar() -> any Bar { ... }
struct Foo<T: Bar> {
var bar: T
}
let foo = Foo(bar: bar())
// this can never compile because `bar`'s return type
// is not statically known to conform to `protocol Bar`
// (meanwhile it would be, if this was `some Bar`)
That's why the opaque type feature is extremely different from the feature of existentials. The fact that people could get these concepts mixed up we can do a better job of educating Swift developers and that "some" might not have been the best word for opaque types.
"Opaque type" is not a very self-explanatory term, and "some" is extremely vague. It makes sense if you already know what it means, but if you don't, it gives you no clues about what is meant.
Opaque types are types that are statically known by the compiler at build-time, but are purposefully hidden from the consumers of the API. It probably should have been called something more descriptive, like a "concealed type" with "concealed" used instead of "some", like:
var body: concealed View // compiler knows what type this is, not you
Meanwhile, the concept of existential containers is totally different. The container type is statically known, but what goes inside is a purely dynamic behavior. The underlying type of a given protocol-type object is not known at compile time.
Future Directions to Clarify Swift
I won't pretend to know what will make the dynamic vs. static aspects of Swift less confusing for most developers, because everyone's brain works differently and this is a very abstract and arcane aspect of the language to understand.
Personally I'm a visual thinker and I like the principle of least surprise. For someone like me, I think it would hep if there was a visual aid combined with an explicit, unambiguous, plain-English keyword, which makes it clear that an existential container is a box whose type is statically known but whose contents' type is only known at runtime. We already have a word, "variable", which means "something that's known at runtime." So we could combine that with a visual aid in the form of |
characters to make it clear what's meant:
struct Thing {
typevariable FooType: Foo
var foo: |FooType|
// foo's type is a container type whose contents are of a variable type
// that's only known at runtime
}
... but for someone whose brain works differently than mine, I'm not sure this would be less confusing than the existing behavior. YMMV