Skip to content
This repository has been archived by the owner on Aug 10, 2021. It is now read-only.

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

Closed
benasher44 opened this issue Mar 12, 2019 · 30 comments

Comments

@benasher44
Copy link
Contributor

benasher44 commented Mar 12, 2019

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
Copy link
Collaborator

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
Copy link
Contributor Author

benasher44 commented Mar 13, 2019

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
Copy link
Collaborator

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
Copy link
Contributor Author

benasher44 commented Mar 14, 2019

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
Copy link
Collaborator

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
Copy link
Contributor Author

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
Copy link
Contributor Author

benasher44 commented Mar 14, 2019

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
Copy link
Collaborator

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
Copy link
Contributor Author

benasher44 commented Mar 14, 2019

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
Copy link
Contributor Author

Sorry closed by mistake.

@benasher44
Copy link
Contributor Author

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

@benasher44
Copy link
Contributor Author

benasher44 commented Mar 14, 2019

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
Copy link
Contributor Author

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

@SvyatoslavScherbina
Copy link
Collaborator

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

What exactly do you mean?

@benasher44
Copy link
Contributor Author

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
Copy link
Collaborator

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
Copy link
Contributor Author

  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
Copy link
Contributor Author

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
Copy link
Contributor Author

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

@SvyatoslavScherbina
Copy link
Collaborator

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
Copy link
Contributor Author

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:

  • Words like .standard or .default (there are other names as well) are usually for those that provide a workable default implementation, but there may be cases where you need to access a non-standard/default instance (either by creating your own or otherwise.
  • .shared usually refers to one that should always be used and creating your own instance may not be possible

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
Copy link
Contributor Author

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
Copy link
Collaborator

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
Copy link
Contributor Author

benasher44 commented Oct 28, 2019

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

@jtouzy
Copy link

jtouzy commented Mar 15, 2020

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
Copy link
Collaborator

Any update on this feature ?

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

@LouisCAD
Copy link
Contributor

LouisCAD commented Dec 3, 2020

Shouldn't this issue move to YouTrack?

@SvyatoslavScherbina
Copy link
Collaborator

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

@LouisCAD
Copy link
Contributor

LouisCAD commented Dec 4, 2020

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

@benasher44
Copy link
Contributor Author

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants
@benasher44 @jtouzy @LouisCAD @SvyatoslavScherbina and others