JetBrains / kotlin-native

Kotlin/Native infrastructure
Apache License 2.0
7.02k stars 566 forks source link

[Improvement] Object and companion object methods should export as class methods/vars in Obj-C/Swift #2757

Closed benasher44 closed 3 years ago

benasher44 commented 5 years ago

Hi there! This has been one of the more awkward interactions with Kotlin/Native. Let's say we have an object:

object Test {
    fun foo() {}
}

This gets exported to Obj-C as a class has an initializer and an instance method called foo, which is strange because foo, when used in Kotlin, is essentially static. We get arround this by writing code like this:

object Test {
    fun foo() {}
}

fun testFoo() = Test.foo() // exports to Obj-C as a class called TestKt with a class method called testFoo

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:

class Test {
    companion object {
        fun foo() {}
    }
}

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 init TestCompanion(), 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.

SvyatoslavScherbina commented 5 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))
benasher44 commented 5 years ago

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?

SvyatoslavScherbina commented 5 years ago

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.

benasher44 commented 5 years ago

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?

SvyatoslavScherbina commented 5 years ago

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))
benasher44 commented 5 years ago

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.

benasher44 commented 5 years ago

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.

SvyatoslavScherbina commented 5 years ago

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.

benasher44 commented 5 years ago

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

benasher44 commented 5 years ago

Sorry closed by mistake.

benasher44 commented 5 years ago

I get it’s tricky, so I appreciate hashing this out 😊

benasher44 commented 5 years ago

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

benasher44 commented 4 years ago

Would making these Obj-C class properties be a compromise? Those would bridge to Swift better.

SvyatoslavScherbina commented 4 years ago

Would making these Obj-C class properties be a compromise?

What exactly do you mean?

benasher44 commented 4 years ago

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.

SvyatoslavScherbina commented 4 years ago

Technically it is possible and expected to be easy. There are some design questions bothering me:

  1. Why do you consider this more discoverable and readable than current solution?
  2. Is there a convention on naming such a shared Objective-C/Swift property? Is this convention common enough?
benasher44 commented 4 years ago
  1. 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.

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

benasher44 commented 4 years ago

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

benasher44 commented 4 years ago

So ideally: SomeObj.companion would access the SomeObj.Companion singleton type

SvyatoslavScherbina commented 4 years ago

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?

benasher44 commented 4 years ago

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.

benasher44 commented 4 years ago

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.

SvyatoslavScherbina commented 4 years ago

SomeObj.Companion.shared (following the theme explained above) feels overly verbose

Sure! But there are also objects that aren't companion, and these ones likely require some class properties for single instances too.

benasher44 commented 4 years ago

Ah right. ObjectName.shared feels like a good fit for those then :)

jtouzy commented 4 years ago

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.

SvyatoslavScherbina commented 4 years ago

Any update on this feature ?

No. This task is in our backlog, but the backlog is quite big.

LouisCAD commented 3 years ago

Shouldn't this issue move to YouTrack?

SvyatoslavScherbina commented 3 years ago

The entire issue? I'm not sure about this. The particular proposal with .shared and .companion? Yes, makes sense.

LouisCAD commented 3 years ago

@benasher44 Is that something you want to do or should I proceed?

benasher44 commented 3 years ago

Filed! https://youtrack.jetbrains.com/issue/KT-43780