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
Implementations
@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.
Objective-C Aside
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.