As noted previously, the Self type makes this impossible to make sound in all cases.
If it were possible to “just” make existentials conform to protocols by delegating through the box, it would have been done ages ago.
As noted previously, the Self type makes this impossible to make sound in all cases.
If it were possible to “just” make existentials conform to protocols by delegating through the box, it would have been done ages ago.
At the end of the day, it really just shouldn't be this difficult to create object abstractions.
Could be as a result of legacy support for classes, or the obfuscation by using the same syntax for either class/protocol inheritence/conformance.
However, as a consumer of the language, it feels all a little broken overall.
As mentioned, it just shouldn't be this difficult to do something so simple. The language should take some responsibility for creating a syntax that only makes a few possible ways of doing this available, otherwise we get into what I call "semantic syntax soup". Where you can no longer derive meaning from the syntax.
It depends on the members. The existential box will erase some type information, and the compiler might not be able to infer what an instance is capable of in a certain context if it's an existential box. This discussion pretty much shows that things are more complex than it seems at first glance.
The compiler could dynamically dispatch the method call to the boxed instance, but in the context where it's used it might not be able to understand the actual result of the dispatch (for example, when dealing with associated types).
No, it's "useful" to the user too: as I mentioned, there's a difference between a "type" and a "type class", and if you're using the type system to enforce invariants, you must understand and leverage that difference. The alternative is simply to not leverage the type system, or to only use part of it, and write something actually simpler (like what @realityworks ended up doing, by removing the relationship between Action
and Transport
).
These discussions about protocols, conformances and existentials can be broadly split in 2 groups:
Also, there's often a lack of understanding of what's an existential box for: in general, for example, it doesn't make much sense to accept any Something
as input to a function. The prime use cases, AFAICT, of existential boxes are:
What you were trying to do, though, was neither simple nor actually sound. Your final solution, that removes the relationship between Action
and Transport
is actually simple instead, and the Swift compiler gladly accepts it.
If you want this design, you can write your own type-erasers. I see some confusion about any
. It's not a constraint - it's a box. The keyword saves you the trouble of writing the box, with the caveat that any Protocol
does not conform to Protocol
- which is solved by allowing programmers to extend the box.
protocol Action { }
protocol ActionA: Action { }
protocol ActionB: Action { }
struct WriteA: ActionA { }
struct AnyActionA: ActionA { init(_ action: some ActionA) { } }
struct WriteB: ActionB { }
struct AnyActionB: ActionB { init(_ action: some ActionB) { } }
protocol Transport {
associatedtype T: Action
func send(action: T)
}
class TransportA: Transport { func send(action: AnyActionA) { } }
class TransportB: Transport { func send(action: AnyActionB) { } }
let transportA = TransportA()
transportA.send(action: AnyActionA(WriteA())) // ✅
transportA.send(action: AnyActionB(WriteB())) // ❌
let transportB = TransportB()
transportB.send(action: AnyActionA(WriteA())) // ❌
transportB.send(action: AnyActionB(WriteB())) // ✅
To be very clear, this is not some implementation deficiency of Swift to be "solved." Saying that all existential types do not conform to their protocols is like saying that all rectangles are not squares. Let's work through a simple example:
Consider the existing standard library type CollectionOfOne<T>
: it is a collection that holds exactly one element of type T
.
Suppose I create two additional types, PairOf<T>
and TripleOf<T>
, which hold exactly two or three elements of type T
, respectively. Together, these three collection types each have a fixed size (1, 2, or 3).
I can create a common protocol, CollectionOfFixedSize
. For the purpose of this example, ignore all syntactic requirements; consider simply the semantic requirement of this protocol that a type which conforms to CollectionOfFixedSize
has to be, well, a collection of a fixed size. Put another way, for any given conforming type, any value of that type has the same number of elements as any other value of the same type.
Each of the three collection types above meet this requirement and can correctly conform to this protocol.
Now, consider the existential type any CollectionOfFixedSize
. By construction, it can represent any value of any collection of fixed size—if it could not, it would not be an existential type.
Therefore, you must be able to write:
var x: any CollectionOfFixedSize = PairOf<Int>(42) // x.size == 2
x = TripleOf<Int>(42) // x.size == 3
As you can see, x
is not a collection of fixed size. This demonstrates that any CollectionOfFixedSize
does not fulfill the semantic requirement of CollectionOfFixedSize
.
Whether the compiler can or can’t synthesize any syntactic requirement is immaterial: it is (and must be) unsound to consider the type any CollectionOfFixedSize
to be a collection of fixed size.
Likewise, if I created a protocol called HasTwoPossibleValues
, I could correctly conform Bool
and any enum with two cases to that protocol, but any HasTwoPossibleValues
can represent more than two possible values.
And, as mentioned before, any FixedWidthInteger
is not a fixed-width integer type.
Not every any P
conforms to the corresponding protocol P
.
i absolutely do see this as an implementation deficiency of swift to be "solved". sometimes you really do want to use the concept of a regular polygon, instead of getting lost in rectangles versus squares.
i can’t count how many times i have wanted to do something like:
protocol ShapeDelegate
{
associatedtype Sides:CollectionOfFixedSize<some BinaryFloatingPoint>
func perimeter(sides:Sides<some BinaryFloatingPoint>)
}
but we cannot do this in swift. so we have to resort to
protocol TriangleDelegate
{
func perimeter(sides:TripleOf<some BinaryFloatingPoint>)
}
protocol RectangleDelegate
{
func perimeter(sides:QuadrupleOf<some BinaryFloatingPoint>)
}
protocol PentagonDelegate
{
func perimeter(sides:QuintupleOf<some BinaryFloatingPoint>)
}
a generic type cannot witness an associated type requirement, only a specialization of a generic type can do that. and this is why people continually complain why existentials cannot do ____, because the language has provided no alternatives to existentials.
To be completely honest, I have trouble following these kinds of discussions, for several reasons.
First of all, it’s not clear to me what actual thing this specific ShapeDelegate example is supposed to represent in practice, or what static invariants we are trying to guarantee here. Maybe a more realistic example would help. Why do you need this abstraction and what would it actually enable in terms of safety, reuse or performance?
More broadly, to make effective use of static types I think it’s important to have a clear understanding both of the abstractions offered by the language and the problem domain being modeled.
I’m not sure if you’re looking for generic associated types, self-conforming existentials, or both. In conclusion, this entire thread leaves me rather confused.
here is a "realistic" example:
protocol BSONDecodable
{
init(bson:BSON.AnyValue<some _Storage>) throws
}
this protocol is hard to provide an init(bson:)
witness for, because it is complicated to go from BSON.AnyValue<some _Storage>
to the thing you actually want to decode from, such as:
extension MyCodableThing
{
init(bson:BSON.ListDecoder<some _Storage>) throws
}
extension MyOtherCodableThing
{
init(bson:BSON.DocumentDecoder<some _Storage>) throws
}
extension YetAnotherCodableThing
{
init(bson:BSON.UTF8<some _Storage>) throws
}
so i end up doing
protocol _BSONListDecodable:BSONDecodable
{
init(bson:BSON.ListDecoder<some _Storage>) throws
}
protocol _BSONDocumentDecodable:BSONDecodable
{
init(bson:BSON.DocumentDecoder<some _Storage>) throws
}
protocol _BSONStringDecodable:BSONDecodable
{
init(bson:BSON.UTF8<some _Storage>) throws
}
but this is an awful API because specifying _BSONListDecodable
, etc. as a generic constraint is almost always a mistake, but users are expected to conform codable types to _BSONListDecodable
, etc. in order to avoid having to implement the “real” BSONDecodable
protocol’s requirement.
i am looking for generic associated types, but many others in this thread are looking for self-conforming existentials. i think it is difficult to recommend what people should be doing besides wishing for self-conforming existentials, because in order to
we need tools in the language to express something like BSONDecodable
with static types.
What @xwu is getting at, and that I believe you understand (though disagree with), is that this issue isn't a technical limitation of Swift, but an explicit design decision to not support, because of the implications for the type system.
I'm guessing that you're aware of this, but any logical system has an inherent conflict between soundness and completeness. Soundness implies that truthful statements in the system can only be used to lead to more truthful statements, while completeness implies that all truthful statements can be produced by the system, one way or another. Less abstractly, for type systems, this means that:
Soundness is great for correctness: "the program compiled, so from a type perspective, it will do what I want". Completeness is great for getting stuff done: "man, I'm glad the compiler got out of my way to let me do what I want".
Unfortunately, you cannot have both, not as an implementation limitation, but as a limitation of the universe. It is not possible to have a system which is both completely sound and complete at the same time: either the system is sound but there will be programs that exist that, although correct, will not compile, or the system is complete but there will be programs which are incorrect that do compile. In a very practical sense, it comes down to tradeoffs, and priorities.
Static type systems tend to lean more toward providing soundness, while dynamic type systems tend to lean more toward providing completeness. Swift, as a matter of intentional design, leans heavily toward soundness, even at the cost of certain programs being impossible to represent.
The examples that both you and @realityworks have provided are pretty clear violations of the Liskov substitution principle, to which Swift adheres. This principle covers some pretty fundamental rules that prop up the soundness of subtyping relationships, and breaking these rules leads to inconsistencies in the type system that cannot somehow be patched up.
Whether intentionally or not, the request here ends up boiling down to Swift relaxing its rules about subtyping relationships — which I think is antithetical to Swift's goals. This also isn't something that can be supported "just a little bit", for example, but not more generally.
(This isn't at all to say that Swift's type system is perfect, or that there aren't countless cases where the compiler can do more to allow correct-but-hard-to-express programs to compile more easily, but this is a pretty foundational set of rules to relax that I simply don't see happening.)
In very reduced terms: it’s not possible to have 1 + 1 occasionally equal 3 without also having some pretty far-reaching consequences.
The non-compiling code might look like that now, but if you see it that way, you're not seeing through to the intent.
There's probably some existing language precedent out there so nobody would need to imagine what we're talking about, but I don't know it.
Here's an example that seems like it should be sound but is illegal in current Swift:
protocol MyDelegateProtocol: AnyObject {
}
class MyDelegate<ViewType: UIView>: MyDelegateProtocol {
func getView() -> ViewType { ... }
}
func getViewIfPossible(delegate: any MyDelegateProtocol) -> UIView? {
if let delegate = delegate as? MyDelegate /* I don't care what specific view type it is */ {
return delegate.getView()
}
return nil
}
I remember back in Swift 5.4 I wanted something like this but I couldn't write it. Maybe it's possible with the associatedtype improvements in Swift 5.7, but it was a frustrating limitation at the time.
(I don't remember the context off the top of my head, but I know it was something like this where the exact generic type didn't matter since it was being casted to a known upper bound.)
No, it’s not. What you’re asking for would require higher-kinder types. The type of delegate
would be <T> where T == MyDelegate, T.ViewType: UIView
. Swift does not allow the type of an expression to have any free variables.
What is ...
? If that works, the rest works. But it's better as
func getViewIfPossible(delegate: some MyDelegateProtocol) -> (some UIView)? {
(delegate as? MyDelegate)?.getView()
}
Regardless, you can't say it like that. MyDelegate
is a generic type, so you can't cast to it without specifying the type argument. You could say as? MyDelegate<UILabel>
, for example, but to enumerate all possible subclasses is... impossible, to say the least.
It's something like that, but not that. If you don't specify, the class constraint is assumed. (UIView
in this case.)
class MyViewDelegate: MyDelegate<UIView> {
override func getView() -> UIView { .init() }
}
class MyTableViewDelegate: MyDelegate<UITableView> {
override func getView() -> UITableView { .init() }
}
getViewIfPossible(delegate: MyViewDelegate()) // UIView?.some
getViewIfPossible(delegate: MyTableViewDelegate()) // UIView?.none
So, just switch to a protocol:
protocol Delegate<View>: MyDelegateProtocol {
associatedtype View: UIView
func getView() -> View
}
extension MyDelegate: Delegate { }
func getViewIfPossible(delegate: some MyDelegateProtocol) -> (some UIView)? {
(delegate as? any Delegate)?.getView()
}
getViewIfPossible(delegate: MyViewDelegate()) // UIView?.some
getViewIfPossible(delegate: MyTableViewDelegate()) // UIView?.some
The protocol should be superfluous, but it's not. any MyDelegate
should work, and would be a step towards what this thread is about. Until that kind of thing gets through Swift Evolution, we'll need protocols that only apply to one type, as a workaround. Simplified:
protocol P<T> { associatedtype T }
final class C<T>: P { }
struct S<T>: P { }
func ƒ<T>() -> some P<T> { S() } // compiles
func ƒ<T>() -> some C<T> { C() } // compiles except for the bug that says it's a redeclaration (https://github.com/apple/swift/issues/53122)
func ƒ<T>() -> some S<T> { S() } // An 'opaque' type must specify only 'Any', 'AnyObject', protocols, and/or a base class
Somebody made a thread about that recently but I can't find it.
In order for this to be useful, you would also have to assume covariance for type parameters, which is exactly the wrong direction for certain types.
// Covariance works here:
class MyDelegate<T: UIView> {
var view: T
}
var x: MyDelegate /* <UIView> */
x = MyDelegate(view: UITableView()) // ok, UITableView is subtype of UIView
x = MyDelegate(view: UILabel()) // ok, UILabel is subtype of UIView
// But it blows up here:
struct Function<Arg: UIView> {
var impl: (Arg) -> Int
func callAsFunction(_ arg: Arg) -> Int { impl(arg) }
}
var x: Function /* <UIView> */
x = Function { (UITableView) -> Int in
return $0.numberOfSections
}
x(UILabel()) // BOOM! type system allows this because UILabel is subtype of UIView
I believe I see to the intent quite clearly, but what I'm trying to get at is that when there is a conflict between the rules of the language and some code, you cannot relax or bend the rules to accommodate that code without also accepting the consequences.
You don't need to go far to see this, and its consequences — Objective-C is a language with a thin veneer of a type system that will happily allow you to implement this; it conveniently ignores the Liskov substitution principle pretty much altogether.
You can represent some of the above Swift code with these Obj-C interfaces:
@protocol Action<NSObject> @end
@interface WifiAction: NSObject<Action>
- (void)broadcastOverWifi;
@end
@interface BluetoothAction: NSObject<Action>
- (void)broadcastOverBluetooth;
@end
@protocol Transport<NSObject>
- (void)send:(id<Action>)action;
@end
@interface WifiTransport: NSObject<Transport>
// Sorry, we only support sending Wifi actions.
- (void)send:(WifiAction *)wifiAction;
@end
@implementation WifiAction
- (void)broadcastOverWifi {
NSLog(@"%@", NSStringFromSelector(_cmd));
}
@end
@implementation BluetoothAction
- (void)broadcastOverBluetooth {
NSLog(@"%@", NSStringFromSelector(_cmd));
}
@end
@implementation WifiTransport
- (void)send:(WifiAction *)wifiAction {
[wifiAction broadcastOverWifi];
}
@end
The above code will compile without warning, and you can easily represent the concepts desired elsewhere in this thread (e.g., NSArray<id<Action>>
for an array of any Action
, NSArray<id<Transport>>
for an array of any Transport
, etc.).
The following will happily compile and run:
id<Action> action = [WifiAction new];
id<Transport> transport = [WifiTransport new];
[transport send:action]; // => broadcastOverWifi
However, the following code will also happily compile and run without warning:
id<Action> action = [BluetoothAction new]; // oops
id<Transport> transport = [WifiTransport new];
[transport send:action]; // 💥
2023-03-02 09:21:00.156 Untitled 4[63954:4630441] -[BluetoothAction broadcastOverWifi]: unrecognized selector sent to instance 0x600000978040
2023-03-02 09:21:00.157 Untitled 4[63954:4630441] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[BluetoothAction broadcastOverWifi]: unrecognized selector sent to instance 0x600000978040'
*** First throw call stack:
(
0 CoreFoundation 0x00000001991303e8 __exceptionPreprocess + 176
1 libobjc.A.dylib 0x0000000198c7aea8 objc_exception_throw + 60
2 CoreFoundation 0x00000001991d2c0c -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x0000000199096660 ___forwarding___ + 1600
4 CoreFoundation 0x0000000199095f60 _CF_forwarding_prep_0 + 96
5 Untitled 4 0x0000000104307b98 -[WifiTransport send:] + 64
6 Untitled 4 0x0000000104307c0c main + 92
7 dyld 0x0000000198cabe50 start + 2544
)
libc++abi: terminating with uncaught exception of type NSException
The assertions being made here by the types are simply violated: clearly, any Transporter
cannot arbitrarily receive any Action
.
Objective-C will happily allow these violations to compile, leaving you to figure out at runtime what went wrong, possibly arbitrarily far away in time and space. Swift makes the choice to disallow this from being written in the first place.
What's important to note is that in this specific case, it's not the rules here that are being bent or broken: the implementation is broken. Objective-C looks the other way when the code claims "sure, this thing that accepts only squares will happily take any other shape", while Swift says "yeah, I don't think so". This is what makes the specific discussion here more involved than "can we lift this restriction from the type sytem?": the request itself is inconsistent.
What's absent from this thread is any sort of consistent answer to what do you want to have happen when someone hands you a BluetoothAction
when you only accept WifiAction
? This is the part that can't just be hand-waved away with a "I don't know, just make it work", because you have to come to some sort of decision.
Objective-C says "I'll let you figure it out at runtime, possibly by crashing, or invoking unexpected behavior", which is one valid answer. Swift could choose to say "I'll deterministically crash", or "I'll continue executing arbitrarily, bitwise-casting the types to make the square peg fit into the round hole at any cost" — those are both valid answers. But they're both answers that go against the guarantees that Swift tries to make everywhere else.
So the question is: we can make this work, but what exact behavior do you want to have happen when arbitrary type A
is passed to a function that takes an unrelated type B
? You cannot simply make this work without answering this question.
I'll add that the discussion here isn't just abstract — the fact that Objective-C allows this sort of operation to happen willy-nilly has resulted not just in buggy code, but in some pretty egregious security violations and breaches. It turns out, it can be pretty easy for an attacker to manipulate behavior to cause unrelated objects to get passed into methods, and when both the compiler and the runtime play along willingly, you can get into all sorts of trouble. Swift tries to avoid this by not playing along at all.
Yeah this is feasible now, makes sense. Like I said earlier I originally ran into this in Swift 5.4, so if I made it a protocol it became far less useful in other ways. Thanks for confirming.
Oh, I don't think it's useful*. But that's how it actually works—it's not hypothetical.
class C { }
struct S<T: C> { }
S() // S<C>
Similarly to how we don't have what this thread is about—an enforcement meaning "only a protocol that derives from another protocol"—we don't have a constraint that enforces only subclasses, rather than "either the superclass, or any subclass". Given that lacking state of affairs, I guess it makes some kind of sense. *Do people make use of it?
That's not how things work.
I understand that some of you think this is what we're talking about. It's not. When I find what we're talking about, in practice, if I remember, I will come back here and update with a link.