Type-safe selectors


(Michel Fortin) #1

Currently in Swift you can get a closure by referring to a method:

  let x = NSString.hasPrefix
  // x is of type NSString -> String -> Bool

Something that would be useful here is if the closure created from Objective-C methods were special in that they could implicitly be converted to a Selector. Instead of writing manually a selector as a string, you'd just have to refer to the method, and you know there's no typo (or else you get a compile-time error). For instance, adding an observer to a NSNotificationCenter would work like this:

  notificationCenter.addObserver(self, selector: MyClass.observeNotification, name: NSSomeNotificationName, object: nil)

This, making sure the correct selector is used for the designated method, seem like it should be somewhat more important in Swift 3 if it includes Evolution Proposal 0005 that suggests many Objective-C methods will be given Swift-specific names.
https://github.com/apple/swift-evolution/blob/master/proposals/0005-objective-c-name-translation.md

But why stop there when you can go one step further and actually improve type-safety? Instead of taking a Selector parameter, the NSNotificationCenter.addObserver method above could request a @convention(selector) closure of this form:

  @convention(selector) AnyObject -> NSNotification -> Void

Under the hood that closure is still a plain selector pointer, but the compiler attaches proper type information to the arguments. Since `addObserver` now declares it wants a selector with the given signature, the compiler can enforce that the arguments and return type for the passed selector are compatible. You

Moreover, the @convention(selector) closure you get can then be used as a normal closure inside a Swift method that can be called from Objective-C:

  @objc func callSelector(selector: @convention(selector) NSString -> String -> Bool) -> Bool {
    let str = NSString(string: "hello")
    return selector(str)("hell")
  }

  let x = NSString.hasPrefix
  // x is of type @convention(selector) NSString -> String -> Bool
  callSelector(x)

So that would make selectors less error-prone because the compiler can type-check them, and you can use selectors in Swift code in a very natural manner.

- - -

This is inspired from the D/Objective-C compiler I prototyped a while ago. There, I made selectors typed with their arguments because:

1. I wanted to preserve D's type-safety while still being able to use selectors, and
2. I needed to decouple selector names from method names in the code; this would later allow me to implement overloading by adding some name mangling in selectors. Obtaining selectors by referring to the method allowed selectors to become an implementation detail while providing a nice way to associate the parameter types.

For reference, here is the meager documentation for that feature:
https://michelf.ca/projects/d-objc/syntax/#selector-literals
And for the selector-name mangling:
https://michelf.ca/projects/d-objc/syntax/#generated-selectors

···

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Joe Groff) #2

Currently in Swift you can get a closure by referring to a method:

  let x = NSString.hasPrefix
  // x is of type NSString -> String -> Bool

Something that would be useful here is if the closure created from Objective-C methods were special in that they could implicitly be converted to a Selector. Instead of writing manually a selector as a string, you'd just have to refer to the method, and you know there's no typo (or else you get a compile-time error). For instance, adding an observer to a NSNotificationCenter would work like this:

  notificationCenter.addObserver(self, selector: MyClass.observeNotification, name: NSSomeNotificationName, object: nil)

This, making sure the correct selector is used for the designated method, seem like it should be somewhat more important in Swift 3 if it includes Evolution Proposal 0005 that suggests many Objective-C methods will be given Swift-specific names.
https://github.com/apple/swift-evolution/blob/master/proposals/0005-objective-c-name-translation.md

But why stop there when you can go one step further and actually improve type-safety? Instead of taking a Selector parameter, the NSNotificationCenter.addObserver method above could request a @convention(selector) closure of this form:

  @convention(selector) AnyObject -> NSNotification -> Void

Under the hood that closure is still a plain selector pointer, but the compiler attaches proper type information to the arguments. Since `addObserver` now declares it wants a selector with the given signature, the compiler can enforce that the arguments and return type for the passed selector are compatible. You

This is a great approach, and it's mostly exactly what I've had in mind for this. Another nice thing about @convention(selector) is that the compiler could also context-free closures, like with @convention(c), by compiling them down to categories with mangled methods.

Moreover, the @convention(selector) closure you get can then be used as a normal closure inside a Swift method that can be called from Objective-C:

  @objc func callSelector(selector: @convention(selector) NSString -> String -> Bool) -> Bool {
    let str = NSString(string: "hello")
    return selector(str)("hell")
  }

  let x = NSString.hasPrefix
  // x is of type @convention(selector) NSString -> String -> Bool
  callSelector(x)

So that would make selectors less error-prone because the compiler can type-check them, and you can use selectors in Swift code in a very natural manner.

I would say that 'let x = NSString.hasPrefix' should still give you a @convention(swift) function value by default; that's what we do with method references in general, but you could ask for a @convention(selector) reference explicitly:

let x: @convention(selector) X -> Y -> Z = X.hasPrefix

-Joe

···

On Dec 4, 2015, at 2:22 PM, Michel Fortin <michel.fortin@michelf.ca> wrote:

- - -

This is inspired from the D/Objective-C compiler I prototyped a while ago. There, I made selectors typed with their arguments because:

1. I wanted to preserve D's type-safety while still being able to use selectors, and
2. I needed to decouple selector names from method names in the code; this would later allow me to implement overloading by adding some name mangling in selectors. Obtaining selectors by referring to the method allowed selectors to become an implementation detail while providing a nice way to associate the parameter types.

For reference, here is the meager documentation for that feature:
https://michelf.ca/projects/d-objc/syntax/#selector-literals
And for the selector-name mangling:
https://michelf.ca/projects/d-objc/syntax/#generated-selectors

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Joe Groff) #3

One wrinkle you need to consider is the different ARC behavior of method families. @convention(selector) would have to be restricted to referencing methods that don't belong to an usual method family or have unusual ownership rules, or else we'd need multiple @convention(init_selector), (copy_selector), (alloc_selector) etc. conventions.

-Joe

···

On Dec 4, 2015, at 2:26 PM, Joe Groff <jgroff@apple.com> wrote:

On Dec 4, 2015, at 2:22 PM, Michel Fortin <michel.fortin@michelf.ca> wrote:

Currently in Swift you can get a closure by referring to a method:

  let x = NSString.hasPrefix
  // x is of type NSString -> String -> Bool

Something that would be useful here is if the closure created from Objective-C methods were special in that they could implicitly be converted to a Selector. Instead of writing manually a selector as a string, you'd just have to refer to the method, and you know there's no typo (or else you get a compile-time error). For instance, adding an observer to a NSNotificationCenter would work like this:

  notificationCenter.addObserver(self, selector: MyClass.observeNotification, name: NSSomeNotificationName, object: nil)

This, making sure the correct selector is used for the designated method, seem like it should be somewhat more important in Swift 3 if it includes Evolution Proposal 0005 that suggests many Objective-C methods will be given Swift-specific names.
https://github.com/apple/swift-evolution/blob/master/proposals/0005-objective-c-name-translation.md

But why stop there when you can go one step further and actually improve type-safety? Instead of taking a Selector parameter, the NSNotificationCenter.addObserver method above could request a @convention(selector) closure of this form:

  @convention(selector) AnyObject -> NSNotification -> Void

Under the hood that closure is still a plain selector pointer, but the compiler attaches proper type information to the arguments. Since `addObserver` now declares it wants a selector with the given signature, the compiler can enforce that the arguments and return type for the passed selector are compatible. You

This is a great approach, and it's mostly exactly what I've had in mind for this. Another nice thing about @convention(selector) is that the compiler could also context-free closures, like with @convention(c), by compiling them down to categories with mangled methods.

Moreover, the @convention(selector) closure you get can then be used as a normal closure inside a Swift method that can be called from Objective-C:

  @objc func callSelector(selector: @convention(selector) NSString -> String -> Bool) -> Bool {
    let str = NSString(string: "hello")
    return selector(str)("hell")
  }

  let x = NSString.hasPrefix
  // x is of type @convention(selector) NSString -> String -> Bool
  callSelector(x)

So that would make selectors less error-prone because the compiler can type-check them, and you can use selectors in Swift code in a very natural manner.

I would say that 'let x = NSString.hasPrefix' should still give you a @convention(swift) function value by default; that's what we do with method references in general, but you could ask for a @convention(selector) reference explicitly:

let x: @convention(selector) X -> Y -> Z = X.hasPrefix


(John McCall) #4

Or, alternatively, encode that information in the type, which would be necessary for full ObjC fidelity anyway, since ObjC ARC allows you to override conventions with attributes.

The flip side is that, of course, that’s quite a bit more complicated.

John.

···

On Dec 4, 2015, at 2:31 PM, Joe Groff <jgroff@apple.com> wrote:

On Dec 4, 2015, at 2:26 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Dec 4, 2015, at 2:22 PM, Michel Fortin <michel.fortin@michelf.ca <mailto:michel.fortin@michelf.ca>> wrote:

Currently in Swift you can get a closure by referring to a method:

  let x = NSString.hasPrefix
  // x is of type NSString -> String -> Bool

Something that would be useful here is if the closure created from Objective-C methods were special in that they could implicitly be converted to a Selector. Instead of writing manually a selector as a string, you'd just have to refer to the method, and you know there's no typo (or else you get a compile-time error). For instance, adding an observer to a NSNotificationCenter would work like this:

  notificationCenter.addObserver(self, selector: MyClass.observeNotification, name: NSSomeNotificationName, object: nil)

This, making sure the correct selector is used for the designated method, seem like it should be somewhat more important in Swift 3 if it includes Evolution Proposal 0005 that suggests many Objective-C methods will be given Swift-specific names.
https://github.com/apple/swift-evolution/blob/master/proposals/0005-objective-c-name-translation.md

But why stop there when you can go one step further and actually improve type-safety? Instead of taking a Selector parameter, the NSNotificationCenter.addObserver method above could request a @convention(selector) closure of this form:

  @convention(selector) AnyObject -> NSNotification -> Void

Under the hood that closure is still a plain selector pointer, but the compiler attaches proper type information to the arguments. Since `addObserver` now declares it wants a selector with the given signature, the compiler can enforce that the arguments and return type for the passed selector are compatible. You

This is a great approach, and it's mostly exactly what I've had in mind for this. Another nice thing about @convention(selector) is that the compiler could also context-free closures, like with @convention(c), by compiling them down to categories with mangled methods.

Moreover, the @convention(selector) closure you get can then be used as a normal closure inside a Swift method that can be called from Objective-C:

  @objc func callSelector(selector: @convention(selector) NSString -> String -> Bool) -> Bool {
    let str = NSString(string: "hello")
    return selector(str)("hell")
  }

  let x = NSString.hasPrefix
  // x is of type @convention(selector) NSString -> String -> Bool
  callSelector(x)

So that would make selectors less error-prone because the compiler can type-check them, and you can use selectors in Swift code in a very natural manner.

I would say that 'let x = NSString.hasPrefix' should still give you a @convention(swift) function value by default; that's what we do with method references in general, but you could ask for a @convention(selector) reference explicitly:

let x: @convention(selector) X -> Y -> Z = X.hasPrefix

One wrinkle you need to consider is the different ARC behavior of method families. @convention(selector) would have to be restricted to referencing methods that don't belong to an usual method family or have unusual ownership rules, or else we'd need multiple @convention(init_selector), (copy_selector), (alloc_selector) etc. conventions.


(Lily Ballard) #5

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

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

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. You'd need
to introduce something else, like @convention(objc_swift) T -> U, that
includes a selector and implicitly converts to both @convention(swift)
and @convention(objc), but this is quickly becoming needlessly complex.
And it still doesn't solve the type-safety issues above.

···

---

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. I'm not sure offhand what the syntax should be,
but I filed a couple of radars a long time ago that also asked for a
syntax to get at the willSet/didSet property observers and the
underlying storage for lazy properties, and suggested that these could
all use the same bit of syntax. I don't know what the syntax should be,
but general meaning of the syntax would be to access pieces of
information about members (where "members" means properties and
methods). As an example of what I'm talking about, if we decided that
the syntax should be to use `` to access things like "foo.storage" as a
value the way you can use it for keywords-as-identifiers, then you could
say things like

    notificationCenter.addObserver(foo, selector:
    Foo.`observeBar.selector`, name: /* ... */)
    self.`lazyBar.storage` = nil
    self.`baz.didSet`(oldValue: qux)

Note that I'm not actually suggesting this is the right syntax to use
(while I like that it re-uses existing syntax, it's also pretty weird),
but the concept is sound.

-Kevin Ballard

On Fri, Dec 4, 2015, at 02:26 PM, Joe Groff wrote:

> On Dec 4, 2015, at 2:22 PM, Michel Fortin <michel.fortin@michelf.ca> wrote:
>
> Currently in Swift you can get a closure by referring to a method:
>
> let x = NSString.hasPrefix
> // x is of type NSString -> String -> Bool
>
> Something that would be useful here is if the closure created from Objective-C methods were special in that they could implicitly be converted to a Selector. Instead of writing manually a selector as a string, you'd just have to refer to the method, and you know there's no typo (or else you get a compile-time error). For instance, adding an observer to a NSNotificationCenter would work like this:
>
> notificationCenter.addObserver(self, selector: MyClass.observeNotification, name: NSSomeNotificationName, object: nil)
>
> This, making sure the correct selector is used for the designated method, seem like it should be somewhat more important in Swift 3 if it includes Evolution Proposal 0005 that suggests many Objective-C methods will be given Swift-specific names.
> https://github.com/apple/swift-evolution/blob/master/proposals/0005-objective-c-name-translation.md
>
> But why stop there when you can go one step further and actually improve type-safety? Instead of taking a Selector parameter, the NSNotificationCenter.addObserver method above could request a @convention(selector) closure of this form:
>
> @convention(selector) AnyObject -> NSNotification -> Void
>
> Under the hood that closure is still a plain selector pointer, but the compiler attaches proper type information to the arguments. Since `addObserver` now declares it wants a selector with the given signature, the compiler can enforce that the arguments and return type for the passed selector are compatible. You

This is a great approach, and it's mostly exactly what I've had in mind
for this. Another nice thing about @convention(selector) is that the
compiler could also context-free closures, like with @convention(c), by
compiling them down to categories with mangled methods.

>
> Moreover, the @convention(selector) closure you get can then be used as a normal closure inside a Swift method that can be called from Objective-C:
>
> @objc func callSelector(selector: @convention(selector) NSString -> String -> Bool) -> Bool {
> let str = NSString(string: "hello")
> return selector(str)("hell")
> }
>
> let x = NSString.hasPrefix
> // x is of type @convention(selector) NSString -> String -> Bool
> callSelector(x)
>
> So that would make selectors less error-prone because the compiler can type-check them, and you can use selectors in Swift code in a very natural manner.

I would say that 'let x = NSString.hasPrefix' should still give you a
@convention(swift) function value by default; that's what we do with
method references in general, but you could ask for a
@convention(selector) reference explicitly:

let x: @convention(selector) X -> Y -> Z = X.hasPrefix

-Joe

> - - -
>
> This is inspired from the D/Objective-C compiler I prototyped a while ago. There, I made selectors typed with their arguments because:
>
> 1. I wanted to preserve D's type-safety while still being able to use selectors, and
> 2. I needed to decouple selector names from method names in the code; this would later allow me to implement overloading by adding some name mangling in selectors. Obtaining selectors by referring to the method allowed selectors to become an implementation detail while providing a nice way to associate the parameter types.
>
> For reference, here is the meager documentation for that feature:
> https://michelf.ca/projects/d-objc/syntax/#selector-literals
> And for the selector-name mangling:
> https://michelf.ca/projects/d-objc/syntax/#generated-selectors
>
>
> --
> Michel Fortin
> michel.fortin@michelf.ca
> https://michelf.ca
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Michel Fortin) #6

Very true. Still, people currently pass selectors around without really bothering about this. It seems to me that restricting @convention(selector) to represent methods having standard behaviour (standard being autoreleased-return with non-consuming arguments) would cover about 99.9% of the use cases.

···

Le 4 déc. 2015 à 17:31, Joe Groff <jgroff@apple.com> a écrit :

One wrinkle you need to consider is the different ARC behavior of method families. @convention(selector) would have to be restricted to referencing methods that don't belong to an usual method family or have unusual ownership rules, or else we'd need multiple @convention(init_selector), (copy_selector), (alloc_selector) etc. conventions.

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Michel Fortin) #7

Something like this?

  view.target = target
  view.action = { (target) in beep() }

Not bad, but wouldn't that be somewhat fragile? Given you must make sure the target is retained, I'm not sure it really make things simpler. And you need to make sure it's not nil either: this just won't work:

  view.action = { beep() }

It looks like an error-prone idiom to me.

···

Le 4 déc. 2015 à 17:26, Joe Groff <jgroff@apple.com> a écrit :

This is a great approach, and it's mostly exactly what I've had in mind for this. Another nice thing about @convention(selector) is that the compiler could also context-free closures, like with @convention(c), by compiling them down to categories with mangled methods.

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Joe Groff) #8

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

Why couldn't this be covered by overloads? It's unlikely we'd be able to import selectors from Objective-C to specific types because of this and other reasons, so we'd still need an untyped Selector type, but we could easily provide well-typed overloads in Swift overlays that convert from typed selectors to untyped.

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

It seems to me that things like performSelector could be replaced by generics in the overlay in a similar way. 'respondsToSelector' is probably fine taking untyped Selectors (with a subtype relationship from @convention(selector) T -> U to Selector).

-Joe

···

On Dec 4, 2015, at 3:27 PM, Kevin Ballard <kevin@sb.org> wrote:

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. You'd need
to introduce something else, like @convention(objc_swift) T -> U, that
includes a selector and implicitly converts to both @convention(swift)
and @convention(objc), but this is quickly becoming needlessly complex.
And it still doesn't solve the type-safety issues above.

---

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. I'm not sure offhand what the syntax should be,
but I filed a couple of radars a long time ago that also asked for a
syntax to get at the willSet/didSet property observers and the
underlying storage for lazy properties, and suggested that these could
all use the same bit of syntax. I don't know what the syntax should be,
but general meaning of the syntax would be to access pieces of
information about members (where "members" means properties and
methods). As an example of what I'm talking about, if we decided that
the syntax should be to use `` to access things like "foo.storage" as a
value the way you can use it for keywords-as-identifiers, then you could
say things like

   notificationCenter.addObserver(foo, selector:
   Foo.`observeBar.selector`, name: /* ... */)
   self.`lazyBar.storage` = nil
   self.`baz.didSet`(oldValue: qux)

Note that I'm not actually suggesting this is the right syntax to use
(while I like that it re-uses existing syntax, it's also pretty weird),
but the concept is sound.

-Kevin Ballard

On Fri, Dec 4, 2015, at 02:26 PM, Joe Groff wrote:

On Dec 4, 2015, at 2:22 PM, Michel Fortin <michel.fortin@michelf.ca> wrote:

Currently in Swift you can get a closure by referring to a method:

  let x = NSString.hasPrefix
  // x is of type NSString -> String -> Bool

Something that would be useful here is if the closure created from Objective-C methods were special in that they could implicitly be converted to a Selector. Instead of writing manually a selector as a string, you'd just have to refer to the method, and you know there's no typo (or else you get a compile-time error). For instance, adding an observer to a NSNotificationCenter would work like this:

  notificationCenter.addObserver(self, selector: MyClass.observeNotification, name: NSSomeNotificationName, object: nil)

This, making sure the correct selector is used for the designated method, seem like it should be somewhat more important in Swift 3 if it includes Evolution Proposal 0005 that suggests many Objective-C methods will be given Swift-specific names.
https://github.com/apple/swift-evolution/blob/master/proposals/0005-objective-c-name-translation.md

But why stop there when you can go one step further and actually improve type-safety? Instead of taking a Selector parameter, the NSNotificationCenter.addObserver method above could request a @convention(selector) closure of this form:

  @convention(selector) AnyObject -> NSNotification -> Void

Under the hood that closure is still a plain selector pointer, but the compiler attaches proper type information to the arguments. Since `addObserver` now declares it wants a selector with the given signature, the compiler can enforce that the arguments and return type for the passed selector are compatible. You

This is a great approach, and it's mostly exactly what I've had in mind
for this. Another nice thing about @convention(selector) is that the
compiler could also context-free closures, like with @convention(c), by
compiling them down to categories with mangled methods.

Moreover, the @convention(selector) closure you get can then be used as a normal closure inside a Swift method that can be called from Objective-C:

  @objc func callSelector(selector: @convention(selector) NSString -> String -> Bool) -> Bool {
    let str = NSString(string: "hello")
    return selector(str)("hell")
  }

  let x = NSString.hasPrefix
  // x is of type @convention(selector) NSString -> String -> Bool
  callSelector(x)

So that would make selectors less error-prone because the compiler can type-check them, and you can use selectors in Swift code in a very natural manner.

I would say that 'let x = NSString.hasPrefix' should still give you a
@convention(swift) function value by default; that's what we do with
method references in general, but you could ask for a
@convention(selector) reference explicitly:

let x: @convention(selector) X -> Y -> Z = X.hasPrefix

-Joe

- - -

This is inspired from the D/Objective-C compiler I prototyped a while ago. There, I made selectors typed with their arguments because:

1. I wanted to preserve D's type-safety while still being able to use selectors, and
2. I needed to decouple selector names from method names in the code; this would later allow me to implement overloading by adding some name mangling in selectors. Obtaining selectors by referring to the method allowed selectors to become an implementation detail while providing a nice way to associate the parameter types.

For reference, here is the meager documentation for that feature:
https://michelf.ca/projects/d-objc/syntax/#selector-literals
And for the selector-name mangling:
https://michelf.ca/projects/d-objc/syntax/#generated-selectors

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #9

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


(Michel Fortin) #10

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
  foo.performSelector(sel)

or this (type-safe using objc_msgSend):

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

or this (type-safe using swift convention):

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

···

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

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Joe Groff) #11

That shouldn't be a problem, since the function would be emitted with the same guaranteed-self convention ObjC methods normally have.

-Joe

···

On Dec 4, 2015, at 3:01 PM, Michel Fortin <michel.fortin@michelf.ca> wrote:

Le 4 déc. 2015 à 17:26, Joe Groff <jgroff@apple.com> a écrit :

This is a great approach, and it's mostly exactly what I've had in mind for this. Another nice thing about @convention(selector) is that the compiler could also context-free closures, like with @convention(c), by compiling them down to categories with mangled methods.

Something like this?

  view.target = target
  view.action = { (target) in beep() }

Not bad, but wouldn't that be somewhat fragile? Given you must make sure the target is retained, I'm not sure it really make things simpler. And you need to make sure it's not nil either: this just won't work:

  view.action = { beep() }

It looks like an error-prone idiom to me.


(Michel Fortin) #12

- (void)action
- (void)action:(id)sender
- (void)action:(id)sender forEvent:(UIEvent *)event

Why couldn't this be covered by overloads? It's unlikely we'd be able to import selectors from Objective-C to specific types because of this and other reasons, so we'd still need an untyped Selector type, but we could easily provide well-typed overloads in Swift overlays that convert from typed selectors to untyped.

Good point. Overloads in framework overlays. That's much simpler than having the compiler trying to infer compatibility as I just suggested.

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)

Nice.

Damned OS X with its target and action properties you have to set separately. Of course you could use the extension below, but it's still bad that you can then set the two properties separately.

  extension NSControl {
    func setTarget<T: NSObject>(target: T?, action: @convention(selector) T -> AnyObject -> Void) {
      self.target = target
      self.action = action
    }
  }

···

Le 4 déc. 2015 à 21:45, Joe Groff <jgroff@apple.com> a écrit :
Le 4 déc. 2015 à 21:49, Brent Royal-Gordon <brent@architechies.com> a écrit :

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Lily Ballard) #13

Why couldn't this be covered by overloads? It's unlikely we'd be able
to import selectors from Objective-C to specific types because of this
and other reasons, so we'd still need an untyped Selector type, but we
could easily provide well-typed overloads in Swift overlays that
convert from typed selectors to untyped.

This can only be covered by overloads when the selector is passed to a
function. So that would work for target/action in iOS, where you use
addTarget(_:action:forControlEvents:). But it won't work for OS X, where
target/action is exposed as two independent properties, because you
can't overload properties.

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.

How can you write that? A @convention(selector) can't actually contain
any executable code, because there's nowhere to store that code (since
the runtime representation of a @convention(selector) is just a SEL,
because the message dispatch is done strictly with the SEL and the
receiver object). So you can't make this type-safe on the receiver,
because the object that receives the selector is by definition the
receiver, and there's no way to force this to be the same type as the
type you generated the selector from.

-Kevin Ballard

···

On Fri, Dec 4, 2015, at 06:45 PM, Joe Groff wrote:
On Fri, Dec 4, 2015, at 06:46 PM, Michel Fortin wrote:


(Michel Fortin) #14

Ok, just realized there's a problem here. This is a valid action method:

  func someAction(sender: UIButton)

It won't work with the above addTarget because it expects a UIButton instead of AnyObject. The signature should say "Self" in the selector type instead of AnyObject so that you can only pass it to addTarget if the control is a UIButton... But you can't use Self in this context, so it still doesn't work.

···

Le 4 déc. 2015 à 21:49, Brent Royal-Gordon <brent@architechies.com> a écrit :

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

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Brent Royal-Gordon) #15

Why couldn't this be covered by overloads? It's unlikely we'd be able
to import selectors from Objective-C to specific types because of this
and other reasons, so we'd still need an untyped Selector type, but we
could easily provide well-typed overloads in Swift overlays that
convert from typed selectors to untyped.

This can only be covered by overloads when the selector is passed to a
function. So that would work for target/action in iOS, where you use
addTarget(_:action:forControlEvents:). But it won't work for OS X, where
target/action is exposed as two independent properties, because you
can't overload properties.

So? I mean, it’s sad that OS X can’t force you to use a selector that matches the target. But you’re still going to get some typing benefits.

Suppose NSControl is bridged to Swift with these properties:

  var target: NSObjectProtocol?
  var action: Selector

And you set it up like so:

  frobButton.target = controller
  frobButton.action = MyViewController.frobnicate

Swift can’t prove that myViewController is of type MyViewController. But this code still proves that:

1. controller is an NSObjectProtocol, and thus a plausible target.
2. frobnicate is a real method on MyViewController (it’s not misspelled, it’s not on the window controller or something).
3. frobnicate is an @objc method.
4. frobnicate will be called if target is a MyViewController (i.e. the user doesn’t have to understand how Objective-C selectors map to Swift methods or worry about whether that particular method has an @objc(replacementName:) attribute).

Now let’s give action a more precise definition:

  var action: (@convention(selector) NSObjectProtocol -> AnyObject -> Void)?

(This doesn’t support no-sender actions, but technically, I don’t think OS X is supposed to allow them. They coincidentally work because the ABI is loosey-goosey in the right places.)

To the above four guarantees, this definition adds a fifth:

5. frobnicate takes one parameter which can accept an AnyObject.

Do these guarantees mean that you can’t ever mismatch the target and action? No. But do they eliminate several other common target/action bugs? Yes! That seems worth doing to me.

And that’s assuming you don’t take any artistic license in the bridging. There’s no reason the Swift overlay can’t do as Michel Fortin suggested and hide the target and action setters, replacing them with a method which provides the same features and guarantees as UIKit’s overloaded calls:

  func setTarget<T: NSObjectProtocol>(target: T?, action: (@convention(selector) T -> AnyObject -> Void)?)

···

--
Brent Royal-Gordon
Architechies


(Michel Fortin) #16

Like this:

  view.action = "_doSomething_UniqueSelector1234_currentModuleName_blahblah"

  extension NSObject {
    func _doSomething_UniqueSelector1234_currentModuleName_blahblah() {
      let target = self
      (target as! MyObject).doSomething()
    }
  }

(Joe Groff suggested it first.) This simply assumes the receiver will derive from NSObject. You also need to set a non-nil target. And note that the closure is context-free, meaning you can't capture variables with it.

I wonder how many methods like this you can add to NSObject before it starts to impact objc_msgSend dispatch performance...

···

On Fri, Dec 4, 2015, at 06:46 PM, Michel Fortin wrote:

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.

How can you write that? A @convention(selector) can't actually contain
any executable code [...]

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Joe Groff) #17

The receiver doesn't have to inherit NSObject. Pure Swift classes on Darwin are id-compatible and can have categories as well.

-Joe

···

On Dec 5, 2015, at 4:30 AM, Michel Fortin <michel.fortin@michelf.ca> wrote:

On Fri, Dec 4, 2015, at 06:46 PM, Michel Fortin wrote:

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.

How can you write that? A @convention(selector) can't actually contain
any executable code [...]

Like this:

  view.action = "_doSomething_UniqueSelector1234_currentModuleName_blahblah"

  extension NSObject {
    func _doSomething_UniqueSelector1234_currentModuleName_blahblah() {
      let target = self
      (target as! MyObject).doSomething()
    }
  }

(Joe Groff suggested it first.) This simply assumes the receiver will derive from NSObject. You also need to set a non-nil target. And note that the closure is context-free, meaning you can't capture variables with it.

I wonder how many methods like this you can add to NSObject before it starts to impact objc_msgSend dispatch performance...


(Michel Fortin) #18

Does that mean that the compiler has to write two categories? one for NSObject and one for the root Swift class?

···

Le 5 déc. 2015 à 10:17, Joe Groff <jgroff@apple.com> a écrit :

The receiver doesn't have to inherit NSObject. Pure Swift classes on Darwin are id-compatible and can have categories as well.

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Lily Ballard) #19

How is this type-safe? You're using as! in there (and silently ignoring
the message if the receiver is wrong wouldn't be any better). If I send
a selector to the wrong receiver, it's still going to crash.

This comes back to my fundamental argument, which is that
@convention(selector) looks like it's type-safe, but there's so many
holes and so many implicit coercions that need to be added that you can
drive a gigantic truck full of unsafety right through it without even
noticing.

-Kevin Ballard

···

On Sat, Dec 5, 2015, at 04:30 AM, Michel Fortin wrote:

Like this:

  view.action = "_doSomething_UniqueSelector1234_currentModuleName_blahblah"

  extension NSObject {
    func _doSomething_UniqueSelector1234_currentModuleName_blahblah() {
      let target = self
      (target as! MyObject).doSomething()
    }
  }

(Joe Groff suggested it first.) This simply assumes the receiver will
derive from NSObject. You also need to set a non-nil target. And note
that the closure is context-free, meaning you can't capture variables
with it.


(Joe Groff) #20

If the selector is expected to exist on an arbitrary `id`, maybe. If it only needs to exist on its `Self` type, it seems to me we can emit the category onto the specific class.

-Joe

···

On Dec 5, 2015, at 7:29 AM, Michel Fortin <michel.fortin@michelf.ca> wrote:

Le 5 déc. 2015 à 10:17, Joe Groff <jgroff@apple.com> a écrit :

The receiver doesn't have to inherit NSObject. Pure Swift classes on Darwin are id-compatible and can have categories as well.

Does that mean that the compiler has to write two categories? one for NSObject and one for the root Swift class?