[swift-evolution] Type-safe selectors

Brent Royal-Gordon brent at architechies.com
Fri Dec 4 20:49:58 CST 2015


> But these 3 forms translate into the 3 distinct types:
> 
> @convention(selector) T -> () -> Void
> @convention(selector) T -> AnyObject -> Void
> @convention(selector) T -> AnyObject -> UIEvent -> Void

Niggle: the third one is actually "T -> (AnyObject, UIEvent) -> Void”—arguments that go in the same parentheses live together in a tuple. Well, technically, the UIEvent is optional too, but that’s beside the point.

Addressing something out of order:

> There's also the confusion around the receiver type T here; a selector
> can't possibly encode the receiver type, because the whole point of
> selectors is the caller doesn't care what the receiver is, it only cares
> how the receiver behaves. You could make the receiver be AnyObject, but
> now you can create a selector from one type and call it with another
> type.

I’m not entirely sure what you’re objecting to here.

If you’re worried about compile-time type checking of the call site, generics can help tie the selector to the object it’s intended for:

	func addTarget<T: NSObject>(target: T?, action: @convention(selector) T -> AnyObject -> Void, forControlEvents: UIControlEvents)

Within the body of addTarget(_:action:forControlEvents:), the type information would be attached to the action *variable*, not the *value* in the variable. This would allow you to support a syntax like this for calling a selector “closure”, instead of relying on performSelector or NSInvocation:

	action(target)(sender)

I believe that natively, this would only allow a ‘target’ that ‘is’ whatever class the action was taken from. Anything less wouldn’t typecheck.

Casting from a typed @convention(selector) closure to a plain Selector would indeed strip away its type information, and casting from a Selector back to an @convention(selector) closure would be unsafe. Perhaps Selector would become “UnsafeSelector" and you’d need an unsafeBitcast() to cast it back into a callable typed selector.

Or are you objecting to the fact that, if the Swift bridging doesn’t correctly reflect the semantics, Objective-C code can still do the wrong thing? Objective-C code can *always* do the wrong thing.

> But the only way to handle this in a reasonable fashion is to allow
> these 3 types to implicitly coerce to each other, which seems like a bad
> idea and removes a lot of the benefit of trying to strongly type them.

Or you can bridge it with several overloaded signatures:

	// This is what’s actually in the Objective-C
	func addTarget(target: AnyObject?, action: UnsafeSelector, forControlEvents: UIControlEvents)

	// These three are added by the bridging layer
	func addTarget<T: NSObject>(target: T?, action: @convention(selector) T -> () -> Void, forControlEvents: UIControlEvents)
	func addTarget<T: NSObject>(target: T?, action: @convention(selector) T -> (AnyObject) -> Void, forControlEvents: UIControlEvents)
	func addTarget<T: NSObject>(target: T?, action: @convention(selector) T -> (AnyObject, UIEvent?) -> Void, forControlEvents: UIControlEvents)

This looks slightly gross, but it’s just reflecting the underlying sloppiness of the target-action mechanism. And it would have helped me a couple months ago, when I forgot that UIGestureRecognizer does *not* have a form which takes an event and lost an hour trying to write a gesture recognizer handler which inspected it. (For added fun, this broke in different ways on the simulator and device.)

> Furthermore, how would you even handle this for methods that take
> selectors of arbitrary types, e.g. respondsToSelector() or various obj-c
> runtime methods?

By taking a Selector, just as it works currently:

	func respondsToSelector(selector: UnsafeSelector) -> Bool

By bridging it as a generic method:

	func respondsToSelector<Args, Return>(selector: @convention(selector) Self -> Args -> Return) -> Bool

By bridging it with Any, which permits any tuple, and otherwise can’t bridge to Objective-C:

	func respondsToSelector(selector: @convention(selector) Self -> Any -> Any) -> Bool

(I *think* the first Any is technically unsound—it should be a bottom type—but a selector with an Any would be uncallable anyway.)

I can think of a few options.

> Allowing implicit conversion to a single common form
> like `@convention(selector) () -> Void` is no better than keeping the
> current Selector (and is in fact worse because it implies strong typing
> where there is none), and keeping the current Selector in addition to
> @convention(selector) is not a great solution either (it leaves the
> language as more complex, without really providing the strong typing
> that @convention(selector) looks like it's trying to do).

The way I see it, @convention(selector) is an opt-in form of strong typing. You can work with bare selectors if you want, just as you can pass around AnyObjects if you want, but when you *do* use @convention(selector) you get extra safety checks.

And, to add a carrot, you could probably get benefits at the call site too—I see no reason Swift couldn’t infer the proper type based on the type of the target parameter if you said:

	incrementButton.addTarget(self, action: .increment, forControlEvents: .PrimaryActionTriggered)

> My simpler proposal here would be to simply embrace the fact that
> selectors are weakly-typed, to say that any API that wants type safety
> should be changed to just take a closure (or to have an overload that
> does), and then to just have a bit of Swift syntax that gives you the
> selector for any method.

In "should be changed to just take a closure”, the “just” carries a lot of water. For instance, that “just” includes redesigning the entire target-action system in two UI frameworks. And how does the concept of dispatching to the first responder apply here? Or is the responder chain basically dead to Swift?

Beware the word “just”.

-- 
Brent Royal-Gordon
Architechies



More information about the swift-evolution mailing list