native
native copied to clipboard
Use named parameters for Obj-C method name translation
The current Obj-C name generation makes clunky methods that don't feel like Dart or like Obj-C. I think it would be much better if it used heuristics like Swift/Obj-C interop, and signature segments were converted to named parameters instead.
E.g., NSNotificationCenter has
addObserverForName:object:queue:usingBlock:, which- ffigen renders as
addObserverForName_object_queue_usingBlock_(NSString? name, ObjCObjectBase? obj, NSOperationQueue? queue, ObjCBlock<ffi.Void Function(NSNotification)> block), while - Swift renders as
addObserver(forName name: NSNotification.Name, object obj: Any?, queue: OperationQueue?, using block: @escaping @Sendable (Notification) -> Void )
I don't see why Dart couldn't make it addObserver({NSString name, ObjCObjectBase? object, NSOperationQueue? queue, ObjCBlock<ffi.Void Function(NSNotification)> block, or any other number of slight variations using heuristics of Obj-C naming conventions (like handling "with" and "from", if param names aren't always good enough).
This would give results more like Obj-C, more like idiomatic Dart, more like Swift, and more readable all around. They would be slightly less deterministically discoverable, but I would gladly trade that slight inconvenience for better readability and more idiomatic code. Especially since realistically I think we can expect most people to do what I'm doing, which is using IDE autocomplete rather than copy/pasting Obj-C signatures and manually translating them.
There are a lot of ObjC methods that differ only by their param names. Eg in NSString:
- compare:options:
- compare:options:range:
- compare:options:range:locale:
The compare base names would clash, so we'd have to fall back to disambiguating by adding numbers. These would become:
NSComparisonResult compare(NSString string, {required NSStringCompareOptions options})
NSComparisonResult compare1(NSString string, {required NSStringCompareOptions options, required NSRange range})
NSComparisonResult compare2(NSString string, {required NSStringCompareOptions options, required NSRange range, required NSObject locale})
// ^- Deduped.
Even so, this might still be more user friendly. WDYT?
I forgot we can't overload in Dart. Hmm.
I expect code will be much more readable with the arbitrary de-duping, but the numbers are kind of weird.
For writing code, selecting the right variant from auto-complete will likely be harder. But accidentally passing params in the wrong order if they have the same type would be much harder, which is good.
And of course, a whole lot of methods won't have collisions. So I'm leaning toward thinking that it's probably a net win to use named parameters. Plus, we already have all the same de-duping problems for a Swift generator, right?
@liamappelbe @stuartmorgan-g
FWIW, I think that I prefer the previous syntax. In the process of converting package:cupertino_http, I'm seeing changes like this:
- URLCache._(ncb.NSURLCache.alloc().initWithMemoryCapacity_diskCapacity_directoryURL_(
- memoryCapacity,
- diskCapacity, directory == null ? null :
- uriToNSURL(directory)));
+ URLCache._(ncb.NSURLCache.alloc().initWithMemoryCapacity$1(
+ memoryCapacity,
+ diskCapacity: diskCapacity,
+ directoryURL: directory == null ? null : uriToNSURL(directory)));
Previously I could just copy the selector from Apple's documentation (example) and replace the colons with underscores.
Now I have to trigger my IDE's autocomplete with the first part of the selector and then scroll through the matches until I find one that looks like the objective-c method.
Even when there is only matching Dart method, it still isn't as easy to go from the objective-c to Dart call syntax.
I'm seeing changes like this
That change looks like a huge improvement in readability to me.
Even when there is only matching Dart method, it still isn't as easy to go from the objective-c to Dart call syntax.
In cases where we have to do the de-duping, sure. But as I said above:
I expect code will be much more readable [...] [...] accidentally passing params in the wrong order if they have the same type would be much harder, which is good.
Those two advantages last for the entire existence of the code, and affects everyone interacting with it. The drawback you are describing applies only once, and affects only the author.
I would much, much rather have code that is easier to read—and therefore review—and harder to accidentally add runtime bugs to than code that is easier to type.
Previously I could just copy the selector from Apple's documentation (example) and replace the colons with underscores.
Sounds like a tooling problem. We're currently running a GSoC project that's experimenting with the use of LLMs to translate native doc snippets to their equivalent Dart binding snippets: https://github.com/dart-lang/native/tree/main/pkgs/native_doc_dartifier (currently the tool only supports JNIgen with plans to extend to FFIgen next and add a chrome extension)
For this specific use case there could be a standalone VSCode extension that converts Obj-C signatures to the exact method without using an LLM if we add helpful annotations on each member and look them up via dart analyzer.
I'm seeing changes like this
That change looks like a huge improvement in readability to me.
Beauty is in the eye of the beholder, I guess ;-)
Even when there is only matching Dart method, it still isn't as easy to go from the objective-c to Dart call syntax.
In cases where we have to do the de-duping, sure. But as I said above:
I expect code will be much more readable [...] [...] accidentally passing params in the wrong order if they have the same type would be much harder, which is good.
Those two advantages last for the entire existence of the code, and affects everyone interacting with it. The drawback you are describing applies only once, and affects only the author.
I would much, much rather have code that is easier to read—and therefore review—and harder to accidentally add runtime bugs to than code that is easier to type.
Beauty is in the eye of the beholder, I guess ;-)
I didn't say beauty though, I said readability. Would you disagree that it's objectively true that accidentally swapping the memory capacity and disk capacity would be much harder (both to write, and to miss in review) in the second version than the first?
It is definitely objectively the case that the second version reads much more like an actual Obj-C method call.
Beauty is in the eye of the beholder, I guess ;-)
I didn't say beauty though, I said readability. Would you disagree that it's objectively true that accidentally swapping the memory capacity and disk capacity would be much harder (both to write, and to miss in review) in the second version than the first?
I agree that accidentally swapping the parameters is harder in the new formulation.
I'm not sure about the readability though. In the example that I gave, the named parameters are just stutter - does prefixing the variable diskCapacity with the name diskCapacity really help you when reading this code (or maybe I'm missing what makes it more readable)?
URLCache._(ncb.NSURLCache.alloc().initWithMemoryCapacity$1(
memoryCapacity,
diskCapacity: diskCapacity
directoryURL: directory == null ? null : uriToNSURL(directory)));
It is definitely objectively the case that the second version reads much more like an actual Obj-C method call.
I think that the new version reads more like a Swift method call but it does not look more like an Objective-C method call to me. The new version makes parts of the selector look like parameters, which is not at all how Objective-C works.
I don't have strong feelings about this but this change does not feel like a net improvement to me.
I agree that accidentally swapping the parameters is harder in the new formulation.
I'm not sure about the readability though. In the example that I gave, the named parameters are just stutter - does prefixing the variable
diskCapacitywith the namediskCapacityreally help you when reading this code (or maybe I'm missing what makes it more readable)?
Maybe we use the term "readability" differently. If there are two ways code could be written, and when I read one I can easily tell if the code does what the author intended to do and the other I can't, I would describe the one where as I can as more readable. Being able to understand what the code actually does is part of readability for me.
Also, I think you are over-anchoring on the specific case of the argument being a variable whose name is the parameter. In cases where that's not true, which I find pretty common, I do in fact find named parameters much easier to understand as I'm reading them than positional parameters in non-trivial methods.
The new version makes parts of the selector look like parameters, which is not at all how Objective-C works.
It's not how it works at a technical language implementation level, but parts of Objective-C selectors absolutely look like parameter names in method calls. It's a core part of Obj-C naming conventions:
If a method takes one or more arguments, the name of the method should indicate each parameter:
The part of a method signature preceding each : in a signature is explicitly supposed to serve the same purpose as a parameter name in languages like Dart and Swift. That's why Swift methods translated from Obj-C look the way they do.
(The only part of the example that looks wrong to me other than the de-duping token is that the part of the signature that is, by Obj-C naming convention, the "name" of the first parameter, isn't extracted, the way it is in Swift. Ideally we would use the same heuristics Swift does for things like fooWithBar:->foo(bar:).)