Closed benasher44 closed 3 years ago
See #1549
Ideally, companion object member would be exported as class members in Obj-C/Swift as well.
(Companion) objects are objects. They can implement interfaces etc. So this doesn't seem correct to unconditionally export all object members as class members.
(init should return a new instance every time
print(NSNumber(value: 42) === NSNumber(value: 42))
Hm I see that’s a good point. It would be nice then to unify the API around getting the shared/companion instance then in Obj-C/Swift: Foo.shared would get the shared instance of Foo object. If Foo were a class, Foo.sharedCompanion would get the shared instance of Foo’s companion object. Thoughts?
I don't find Foo()
and Foo.Companion()
confusing.
Even among standard iOS API there are examples of Objective-C factory methods represented as init(...)
in Swift. Also in some cases init
methods may return cached instances. The combination of these two approaches is thus supposed to be familiar to Objective-C/Swift developers.
Also, neither Objective-C nor Swift has any concept of singletons in the language itself, so there is no natural representation for Kotlin singletons.
It’s confusing because those 2 methods are supposed to return new instances in Obj-C. But, when used in Kotlin, you’re used a shared instance. Is that not accurate?
It’s confusing because those to things are supposed to return new instances.
Is this statement applicable to standard frameworks? See my comment above:
print(NSNumber(value: 42) === NSNumber(value: 42))
I agree with you on factory methods themselves. The issue is if you try to do something like NSNumber.maxInt in Kotlin. In Kotlin, I think you would do:
class NSNumber {
companion object {
val maxInt: NSNumber…
}
}
To access this exported to Obj-C, you have to do NSNumberCompanion().maxInt, which feels strange because you’re creating a new companion to access what is essentially static.
While I agree there is no keyword that means singleton in Obj-C/Swift, there are well-established patterns in the standard library. See NSUserDefaults and NSFileManager in Foundation or globals in UIKit (UIApplication, UIScreen, etc.). Swift has language-level support for this by making all static let
vars lazily loaded using a dispatch_once under the hood (somewhat closely matching by lazy
in Kotlin), which removes most of the work/effort to make singletons.
Also I believe the reason that NSNumber(value: 42) === NSNumber(value: 42)
works is because NSNumbers use tagged pointers (and possibly only in some trivial scenarios in 64bit). I don’t think the === behavior there is always the case.
To access this exported to Obj-C, you have to do NSNumberCompanion().maxInt, which feels strange because you’re creating a new companion to access what is essentially static.
You aren't creating a new companion. As I've mentioned above, even standard library itself doesn't follow the convention init(...)
= "create new instance".
NSUserDefaults and NSFileManager UIScreen
Doesn't seem singletons to me, since these classes have custom initializers/factories.
UIApplication
Can hold arbitrary user subclass instance, so isn't quite typical singleton too.
Swift has language-level support for this by making all static let vars lazily loaded using a dispatch_once under the hood (somewhat closely matching by lazy in Kotlin), which removes most of the work/effort to make singletons.
Java has language-level support for this by making all static vars lazily loaded using a clinit under the hood. But there is still a reason behind not exporting all companion object members as Java statics by default.
I agree that accessing Kotlin object members is not idiomatic yet, but there is no simple solution for this problem.
As I've mentioned above, even standard library itself doesn't follow the convention init(...) = "create new instance".
Where is that not the case? The NSNumber scenario is special. That’s either a library or compiler optimization that is non-standard. In general, Swift init returns a new object. Especially with reference counting, it’s supposed to return a +1 retain count object, and the compiler for Obj-C recognizes this naming convention and requires the annotation NS_RETURNS_NOT_RETAINED (even for ARC) if you mean otherwise. I think here we’re okay in Obj-C because the method is just called companion
. I’m not sure about swift though. I’ve never thought to use NS_SWIFT_NAME to make an Obj-C method look like a Swift initializer.
For the examples I mentioned, I think you’re referring to UIApplicationDelegate, not UIApplication. For the Foundation examples, those are great examples where the singleton naming hints that you can create instances if needed, but there are “default”/“standard” (singleton) instances available for convenience. Whereas UIApplication only uses “shared” in the name, which hints that you shouldn’t create one (I think it also has compiler annotations that also strongly discourage that, but I can’t remember).
Sorry closed by mistake.
I get it’s tricky, so I appreciate hashing this out 😊
FWIW, I think doing something like Foo.companion (same in Swift, so no init) to access the companion object of Foo and Foo.shared to access Foo, if it were an object would most closely match semantics (along with marking the initializers as unavailable) in Obj-C without going with the full static approach).
Would making these Obj-C class properties be a compromise? Those would bridge to Swift better.
Would making these Obj-C class properties be a compromise?
What exactly do you mean?
Oh whoops. That's very vague sorry. I meant to say: would it be possible to make "companion" an Obj-C class property? So SomeClass.Companion
would feel the same in Obj-C and Swift.
Technically it is possible and expected to be easy. There are some design questions bothering me:
shared
Objective-C/Swift property? Is this convention common enough?As an iOS dev, once you become familiar with companion objects in Kotlin, it's then very bizarre to go to Swift and see that you access it by calling an init
for a type that's understood to be a singleton. Since it's a singleton, you'd expect some kind of static way of accessing it. While it may be static under-the-hood, the API doesn't read that way.
Yep modern Obj-C APIs bridge to Swift this way, and nearly all of Apple's Obj-C APIs have been modernized this way. Some examples from Foundation: FileManager.default, UserDefaults.shared, NSRunLoop.mainRunLoop. I can provide similar examples from other Apple frameworks as well.
Obj-C class properties were introduced back when Swift 3 was released as a way to improve Swift interop for Obj-C statics/singletons: https://useyourloaf.com/blog/objective-c-class-properties/, so I think it'd be a natural fit here :)
So ideally: SomeObj.companion
would access the SomeObj.Companion singleton type
Yep modern Obj-C APIs bridge to Swift this way, and nearly all of Apple's Obj-C APIs have been modernized this way. Some examples from Foundation: FileManager.default, UserDefaults.shared, NSRunLoop.mainRunLoop. I can provide similar examples from other Apple frameworks as well.
You have just confirmed the opposite: there is no common convention on naming: some APIs use shared
, others use default
/main
/whatever. So what is the most common? Is there an "official" naming convention described in Swift language documentation?
I didn't mean to confirm a common convention around naming- only that class properties are a popular way to bridge singletons to Swift, which would be nice to see here :)
There are some themes in Foundation though:
In this case, SomeObj.companion returning the shared SomeObj.Companion instance seems reasonable and clear. SomeObj.Companion.shared (following the theme explained above) feels overly verbose given the number of characters typed without having even accomplished anything useful yet.
Swift does have an API design guide, but it doesn't mention singletons specifically: https://swift.org/documentation/api-design-guidelines/.
IMO, the best we can do is draw on examples from Foundation and how it has evolved to become friendlier to Swift.
SomeObj.Companion.shared (following the theme explained above) feels overly verbose
Sure! But there are also object
s that aren't companion
, and these ones likely require some class properties for single instances too.
Ah right. ObjectName.shared
feels like a good fit for those then :)
Any update on this feature ?
For now, a "workaround for syntax" is to add a temporary extension in your Swift codebase (in case of object
usage in Kotlin) :
extension ObjectName {
static let shared = ObjectName()
}
It doesn't block you the object initializer, but at least you have a classic Swift syntax when you want to use the singleton everywhere. And if this feature is implemented, you will just need to remove the extension to make it compatible.
Any update on this feature ?
No. This task is in our backlog, but the backlog is quite big.
Shouldn't this issue move to YouTrack?
The entire issue? I'm not sure about this.
The particular proposal with .shared
and .companion
? Yes, makes sense.
@benasher44 Is that something you want to do or should I proceed?
Hi there! This has been one of the more awkward interactions with Kotlin/Native. Let's say we have an
object
:This gets exported to Obj-C as a class has an initializer and an instance method called
foo
, which is strange becausefoo
, when used in Kotlin, is essentially static. We get arround this by writing code like this:It would be great if
Test.foo()
were instead exported to Obj-C such that it could be called like[Test foo]
to match how you'd call it in Kotlin. For companion objects, it gets a bit stranger. Let's say we have a similar setup:In Obj-C, this gets you a class called
TestCompanion
, which you can access by calling[TestCompanion companion]
(funny looking, but it at least matches singleton conventions), but then in Swift it looks like an initTestCompanion()
, which feels odd (init should return a new instance every time, but here I think it returns the static instance). Ideally,companion object
member would be exported as class members in Obj-C/Swift as well.Thoughts on this? This feels like a worthwhile improvement, which would avoid having to frontload a discussion about Kotlin objects and companion objects, if you're just an iOS/macOS developer trying consume a MPP library.