[swift-evolution] Type-safe selectors

Michel Fortin michel.fortin at michelf.ca
Fri Dec 4 20:46:12 CST 2015

Le 4 déc. 2015 à 18:27, Kevin Ballard <kevin at sb.org> a écrit :

> The @convention(selector) as proposed is a neat idea, but it will
> completely break target/action. This is because the
> @convention(selector) is a strongly-typed function signature, but
> target/action relies on the fact that it can provide 2 parameters to the
> method and this will work with any method that matches one of the 3
> forms (2 forms on OS X):
> - (void)action
> - (void)action:(id)sender
> - (void)action:(id)sender forEvent:(UIEvent *)event
> 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
> 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.

I don't think it removes the benefit of strongly typing them. The reason to strongly type selectors is to avoid generating buggy code such as when passing an Int to a method that expects a pointer. Implicitly converting "@convention(selector) T -> (AnyObject, UIEvent) -> Void" to "@convention(selector) T -> () -> Void" is perfectly safe (we know it and do all the time) and the compiler could verify that for you before allowing the conversion to happen.

I agree with you though that if this implicit conversion does not work, it's going to be annoying despite the type-safety benefits.

> 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.

That's indeed true. I hadn't realized. The fact that the selector lives separately from its target object makes things difficult because the expected target type is almost always going to be AnyObject. Implicit conversions cannot happen safely in the direction SubType to BaseType for the arguments, including the target object. That makes Joe Groff's approach the only type-safe solution: make an extension of the base object and generate a method that does what you want. Which means that instead of this:

	view.action = MyObject.doSomething

you could write this:

	view.action = { (target: AnyObject) in {
		(target as! MyObject).doSomething()

...which is safe. Maybe the compiler should just auto-generate that boilerplate for you.

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

Having a @convention(selector) does not mean you have to get rid of Selector everywhere. When you have a selector of an unknown kind (such as in respondsToSelector), just use Selector, as you do currently. Perhaps it should be renamed to UnsafeSelector.

And with an UnsafeSelector you should be able to unsafely convert it to a selector closure that you can then call:

	let sel: UnsafeSelector = NSString.lengthOfBytesUsingEncoding
	// sel has no type information for the target or arguments
	let closure = sel.convertTo<@convention(selector) NSString -> UInt -> Int>()
	// now that we've reinjected the type information, we can call it
	let result = closure("hello")(NSASCIIStringEncoding)

One important reason for having @convention(selector) is so you can call the selector from Swift.

> I also worry that allowing something like @convention(selector) would be
> confusing, because it would look like the following two code snippets
> should be identical:
>    foo.performSelector(Foo.handleBar)
> and
>    let sel = Foo.handleBar
>    foo.performSelector(sel)
> But this can't work because it requires the ability to convert from
> @convention(swift) T -> U into @convention(selector) T -> U, which can't
> work because not all closures will have associated selectors.

No, you certainly can't convert from @convention(swift) to @convention(selector). The reverse is possible however. That's why my first idea was to have "Foo.handleBar" be @convention(selector) with implicit conversion to @convention(swift) when necessary. Then Joe Groff suggested that this should only happen if you explicitly specify the type of "sel" to be @convention(selector). It makes sense from a performance standpoint that you don't want to wrap calls more than necessary.

Like you I feel like the two code snippets above should be equivalent. But if there's going to be a performance cost because of an implicit conversion from @convention(selector) to @convention(swift), then I think Joe's idea is the right one: force the user to specify early the type he needs and don't implicitly convert between conventions. Which gives you this:

	let sel: UnsafeSelector = Foo.handleBar

or this (type-safe using objc_msgSend):

	let sel: @convention(selector) Foo -> () -> () = Foo.handleBar

or this (type-safe using swift convention): 

	let closure = Foo.handleBar
	closure(foo)() // likely the most performant

Michel Fortin
michel.fortin at michelf.ca

More information about the swift-evolution mailing list