Kotlin / KEEP

Kotlin Evolution and Enhancement Process
Apache License 2.0
3.29k stars 357 forks source link

Kotlin statics and static extensions #348

Open elizarov opened 1 year ago

elizarov commented 1 year ago

This is an issue for discussion of Kotlin statics and static extensions proposal. This proposal is the culmination of our research and design on KT-11968 Research and prototype namespace-based solution for statics and static extensions.

The full text of the proposal is here.

Please, use this issue for the discussion on the substance of the proposal. For minor corrections to the text, please open comment directly in the PR #347.

OPEN ISSUE: This proposal has a major open issue on what kind of declaration syntax to use for static members: static sections or static modifiers. See Static section vs static modifier for details on their pros and cons. The text of the proposal is written showing both alternatives side by side.

Let's use this issue for an ad-hoc voting on the syntax via reactions:

Please, read the proposal before voting!. If you are just excited about the future introduction of statics in Kotiln, then react with πŸ‘. If you don't like the idea at all, then react with πŸ‘Ž and explain your concerns in the comments.

JakeWharton commented 1 year ago

Great write-up, but there doesn't seem to be any information about the ABI of platforms other than the JVM. What's the ABI of JS, and is it blocked/require ES2015 output from the compiler? Does the Objective-C interop use class methods or something else?

Also curious if external static function declarations work on JVM and JS?

SPC-code commented 1 year ago

(on behalf of @altavir) Looks nice. I definitely like blocks more than modifier. It follows the overall tendency of using scopes to designate changes of code block semantics. It also encourages to group static statements together instead of placing them in random places in the class.

Also it would be nice to consider using namespace used in early design instead of static. I understand that static is familiar from other languages. But namespace better corresponds to the role the feature plays in Kotlin. In Java, where everything is a class and must be dynamically instantialized, it makes sense. But in Kotlin we have top level functions, objects etc. The concept of static does not make a lot of sense. On the other hand namespace makes a lot of sense. It emphasizes the fact that we have a concept of a named scopes (packages, objects, classes, etc) and we can create namespace hierarchies.

The question of interoperability with JS is also interesting in the context of https://youtrack.jetbrains.com/issue/KT-46164

axelfontaine commented 1 year ago

The concept of "static sections" could be generalized to "modifier sections" with equivalent private and internal sections. Each modifier section would then apply its modifier implicitly to all contained members.

In cases where it would favor readability, these sections could be collapsed to regular modifiers as used presently.

While the proposal explicitly rejects such a hybrid approach for static, the more general case may be worth considering nonetheless.

mikehearn commented 1 year ago

Great proposal. Only one observation for the JVM mangling scheme - $ is not that readable, but the other proposals might be confusing e.g. I wouldn't immediately think that getBackgroundColor would map to Color.static.background if I saw it in a stack trace.

Other alternatives:

Maxr1998 commented 1 year ago

Thanks for the proposal, this looks very promising. Personally, I lean towards static sections, the only thing I don't appreciate is the verbosity of a single static constant property declaration. I understand that supporting both static sections and static modifiers generally is not an option, but what if this would be dependent on the declaration type?

E.g., static sections could be required for functions, extensions and getters, whereas only static constant properties could support the static modifier syntax (exclusively, or in addition to static sections). This would solve the verbosity issue and only slightly increase complexity. The code style discussion remains, but for constants only it isn't as impactful, from my perspective.

streetsofboston commented 1 year ago

Great proposal!

Few concerns that popup in my head.

Will companion objects be deprecated? I hope not, since these can implement an interface. Or will static interfaces allow for a similar way of having a static 'reference' (in code that uses it) that satisfies that interface?

I'm not a fan of static objects being effectively a namespace (no this reference). The word object implies there is a this reference (to that object). I would favor a new keyword, eg namespace.

mikehearn commented 1 year ago

Another suggestion - the distinction between object and static object is unfortunate. Complexity that will exist forever. Is it maybe not possible to just automatically change how it's compiled depending on whether the object actually contains any state. To avoid binary compatibility breaks on the JVM, the instance and forwarding methods can just always be generated unless you explicitly opt out. That would avoid the need to explain the difference which could be difficult, especially to people learning programming for the first time.

mcpiroman commented 1 year ago

Alternative proposals:

  1. instead of static section have static companion object, so that it is symmetric with static object, or
  2. (IMHO better) as mentioned above, replace static object with namespace, thus also having companion namespace (instead of static section).

Pros of 1):

Cons of 1):

But those cons are solved in option 2):

sandwwraith commented 1 year ago

I agree with the suggestion to replace static object with namespace. IMO a lot of people read 'singleton' when they see object, and 'static singleton' doesn't make a lot of sense. As you said yourself:

Static modifier is not allowed on other declarations (including class, interface, and typealias declarations). Rationale: static modifier on a class, interface declarations make no sense, as such declarations are already static by default and do not have access to an outer class instance.

The exact same rationale is also applicable to objects.

SPC-code commented 1 year ago

I also like MyClass.namespace.something() much better than MyClass.static.something().

sandwwraith commented 1 year ago

I even would go further and suggest allowing namespace modifier on ext functions, to replace fun Color.static.myExtension() with namespace fun Color.myExtension(). IMO, the latter nicely reads as 'function in the namespace of Color'. Color.static, on the other hand, introduces some specific entity that is a special case β€” normally, we are allowed to write fun A.B.x() only if B is a class or object inside A. Avoiding special cases and providing concise, but explicit syntax is one of the Kotlin's design cornerstones.

sandwwraith commented 1 year ago

I see there's suggestion for further improvements to use with(Color.static) {} to use static functions without prefix: https://github.com/Kotlin/KEEP/blob/statics/proposals/statics.md#static-scope-projection-reference

Does it mean that regular with(Color) {} will allow only using companion object functions without prefixes, but still would require Color. to call static ones?

mcpiroman commented 1 year ago

@streetsofboston

Will companion objects be deprecated?

It is a non-goal of this proposal to deprecate or to completely replace companion objects.

TheBestPessimist commented 1 year ago

Can someone explain me please the following: in the example Option: Static section syntax, copied below:

class Outer(val one: String, val two: String) {
    static {
        fun createMappings(): List<String> =
            setOf(::one, ::two).map { it.name } // WORKS! No need to write Outer::one, Outer::two
    }
}

because createMappings is a static, this means i don't need an instance of Outer to call the static function. So i can just do

val l: List<String> = Outer::createMappings()

My problem with this is that one and two and it.name are instance variables of Outer, which means that they do not exist in my example above.

The example looks wrong to me, but I assume it's not wrong and I'm just misunderstanding something.

spen37 commented 1 year ago

Overall I love this proposal but I echo the sentiments of using namespace over static object as there is no "object" to speak of.

Also seems to be an unpopular opinion but I favour the modifier syntax over the sections. This is just my personal opinion but I feel the verbosity of a whole separate section makes this feel like a shorthand for just declaring a companion object. On that note, I understand the syntax for static extensions makes a ton of sense when you use sections, but from the modifiers perspective would something like static fun SomeClass.someMethod() make more sense?

Mr-Pine commented 1 year ago

This would conflict with Extension functions as static members but I agree with you and don't really like fun SomeClass.static.extension() either

mcpiroman commented 1 year ago

@TheBestPessimist This works because ::one and ::two are property references (KProperty1s) , which don't have this reference. As suggested, this is the same as writing Outer::one (in any scope).

kyay10 commented 1 year ago

Since "Extension functions as static members" is allowed, I think we absolutely should allow static operators that can be imported as you'd expect. That would allow more controlled scoping of operators. Also, I agree with everyone that namespace makes a ton more sense over static. Obviously it's important to keep the language accessible to newcomers, but I think that static object alone shows how inappropriate the word static is for the Kotlin language. Also, there's an unresolved question here about how you could define an extension static function that is itself an extension on some type (i.e. Int.foo that is within the namespace of MyClass). Perhaps that is a non-issue now, but with "Static scope projection reference", we should have support for using MyClass.static as a context receiver.

Maybe also instead of the MyClass.static syntax we could use something like namespace<MyClass> that was shown as an early sketch. That could be read as namespace of MyClass and would perhaps emphasise that this declaration is not extending a type in the ordinary sense but instead is extending a namespace of that type.

He-Pin commented 1 year ago

I wanted this in Scala 3 to easily add extension methods for both scala's object and Java classes which act as static members. So +1

spen37 commented 1 year ago

@Mr-Pine Ah yes I see. In that case I guess I agree with @sandwwraith's comment on calling it a namespace fun. That way you could also potentially have something funky like a static namespace fun πŸ˜†

mcpiroman commented 1 year ago

Notes:

Option: Static section syntax.

class Color(val rgb: Int) : Parseable<Color> {
    static {
        fun parse(s: String): Color { /* impl */ }
    }
}

Shouldn't fun parse be override?

sandwwraith commented 1 year ago

@spen37 It looks like static namespace fun is also possible with the suggested syntax as static fun Color.static.myExt() or static { Color.static.myExt() }. Not sure there are any compelling use-cases for that. Either syntax is confusing.

Mr-Pine commented 1 year ago

I like namespace instead of static object but would still prefer static in the context of static members.

Having a static extension as a static member will probably always be a little confusing because you have to signal, that it is "double static", but I like @kyay10's of declaring static extensions as static<MyClass>.myExtension() (or namespace<>, or whatever the final Keyword will be) although I'm not perfectly happy with that solution either since I think it will be confusing (at first) why an extension member in a static block (or with a static keyword) is not a static extension. So it may also make sense to consider a syntax for specifying that an extension that is a static member is not also a static extension

dovchinnikov commented 1 year ago

There is a class, there is a class with 1 instance: object, I'd expected a separate keyword for class with 0 instances. I like namespace the most, since it describes exactly what static object is supposed to represent.

quickstep24 commented 1 year ago

Great proposal. The idea of statically implementing an interface is interesting, but I wonder if static interface is the right concept. It implies that the interface "knows" that it will be (must be) implemented statically. An alternative would be:

interface Parseable<T> {
    fun parse(s: String): T
}
class Color(val rgb: Int) : static Parseable<Color> {
    static {
        /*override*/ fun parse(s: String): Color { /* impl */ }
    }
}
deotimedev commented 1 year ago

Great proposal. The idea of statically implementing an interface is interesting, but I wonder if static interface is the right concept. It implies that the interface "knows" that it will be (must be) implemented statically. An alternative would be:

interface Parseable<T> {
    fun parse(s: String): T
}
class Color(val rgb: Int) : static Parseable<Color> {
    static {
        /*override*/ fun parse(s: String): Color { /* impl */ }
    }
}

I like that style of static implementation, but If the interface does not know if it will be implemented statically or not, what would happen if illegal this references were made in it? Example:

interface Parseable<T> {
    fun parse(s: String): T
    fun something() {
        println("I am $this") // no instance of `this` statically
    }
}
rnett commented 1 year ago

For static inheritance, it would also be nice to be able to mix static and non-static abstract methods, e.g.

interface Serializable<T> {
    abstract static fun deserialize(s: String): T
    fun serialize(): String
}

You'd need some way to specify that the static method in the interface is abstract, which is what I used abstract static for in the example. This is achievable anyways by having an interface implement a static interface, but IMO being able to mix them is quite a bit nicer.

edrd-f commented 1 year ago

There is a class, there is a class with 1 instance: object, I'd expected a separate keyword for class with 0 instances. I like namespace the most, since it describes exactly what static object is supposed to represent.

It's indeed confusing to have static as a modifier of object because static does not modify a property of an object. Instead, it removes its essential property, which is having a single instance.

I agree namespace would be better since it makes it clear that's a different concept.

JohannesPtaszyk commented 1 year ago

As I like the grouping aspect that automatically came with companions, I highly favor the grouping syntax here. 😊

sprigogin commented 1 year ago

Great proposal. I've been waiting for this since I started using Kotlin.

Few observations:

  1. The "It is easy to migrate existing companion object declarations to statics" argument in favor of static sections would not apply if IntelliJ offers a refactoring that does this conversion.
  2. It would be more concise and intuitive if static object were replaced with namespace.

Question:

  1. What is the rationale for the "Static section is not allowed inside an inner class" restriction? Is it due to the JVM bytecode limitation?
hfhbd commented 1 year ago

I like the static modifier more; it is more aligned with other languages and directly clear from the function signature if the function is static. With the section, you have to check its location in the class, if this function is static (the same is true for the current compaction object section).

Although I see the current votes, the static init {} of the modifier syntax could be simplified to static { }, making this more symmetrical to init {} and to Java.

Another thing: How do you call a static extension function extending "the static section" of another class:

class Example {
  static fun AnotherClass.static.foo4() {}
}

What's the use-case allowing this?

anod commented 1 year ago

I don't like the idea at all, static extensions are nice to have but not worth to add more complexity to the language (companion objects already provide most of the need) and this probably will impact kmm implementation

SageDroid commented 1 year ago

Looks promising, and no matter how the open issues are resolved, this will definitely be an enrichment for the language!

My thoughts on the static section vs static modifier question:

fun Example.static.foo3() {}
fun Example.static.foo4() {}

class Example {
    static {
        fun foo1() {}
        fun foo2() {}
    }
}
elizarov commented 1 year ago

Great write-up, but there doesn't seem to be any information about the ABI of platforms other than the JVM. What's the ABI of JS, and is it blocked/require ES2015 output from the compiler? Does the Objective-C interop use class methods or something else?

Also curious if external static function declarations work on JVM and JS?

@JakeWharton Thanks! I've added the ABI for non-JVM platforms section to the open issues. We don't have any design for them yet. Kotlin/JVM platform provides the strictest backwards and forwards compatibility guarantees, as well as seamless two-way interoperability, hence the JVM ABI is taken care of first.

elizarov commented 1 year ago

The concept of "static sections" could be generalized to "modifier sections" with equivalent private and internal sections. Each modifier section would then apply its modifier implicitly to all contained members.

@axelfontaine That would be a quite un-Kotlin way to approach things. This would add an extra freedom to the way you write your code that is bound to inflame code-style wars. Readability will suffer for an average piece of Kotlin code, because you'll never know if a modifier is truly absent before a declaration or if you have to look for a "modifier block" that changes it. Static sections are a special case because of their connection to static extensions.

elizarov commented 1 year ago

Great proposal. Only one observation for the JVM mangling scheme - $ is not that readable, but the other proposals might be confusing e.g. I wouldn't immediately think that getBackgroundColor would map to Color.static.background if I saw it in a stack trace.

@mikehearn I've added the first two of your suggestions to the table in the corresponding section. From my experience, spaces in stack-traces would cause readability issues and might get misinterpreted by tools.

JakeWharton commented 1 year ago

Spaces also don't work on Android except on the absolute newest versions so I would entirely reject that as a possibility.

elizarov commented 1 year ago

@streetsofboston @mikehearn @mcpiroman @sandwwraith @spen37 @kyay10 @Mr-Pine @dovchinnikov @edrd-f @sprigogin I've completely missed the fact that static object is bound to cause controversy and did not provide background information on this choice in the design document. I've fixed it now.

I've added a short note at the end of Static objects section and added a whole new section on Static object alternatives and namespaces where I give an in-depth explanation of why namespace did not make a cut. Please, read it.

TL;DR: Think of relation between static object andobject in a similar way as about relation between value class and class — a sort of performance-optimizing feature for experts to use. It might seem that "having an identity" is an integral part of "being an object" in Kotlin, but it is not. E.g., 1 in Kotlin is an object, yet it does not have an identity. Also, keep in mind that the whole static object feature is the least important part of the proposal. It is there only to avoid abuse in the form of "static utility classes" (classes with static-only members). Being the least important part, it must not dictate the choice of concepts and naming for the rest of the design.

kyay10 commented 1 year ago

@elizarov

1 in Kotlin is an object

But 1 is not a type, while an object is a type. When I see an object, I expect it to have a type. Maybe the average developer doesn't view it as such, I'm not sure.

It also feels like the use of static object is a relic of the current use of object for static grouping of things. If Kotlin was designed today, would static object be the most apt solution? static object is obviously subtractive, but it seems to subtract a lot of properties of object, reducing it down to just a name. value class is also subtractive, but it still keeps some fundamental properties of a class.

elizarov commented 1 year ago

But 1 is not a type, while an object is a type. When I see an object, I expect it to have a type. Maybe the average developer doesn't view it as such, I'm not sure.

@kyay10 If you grep the usages of object in Kotlin codebase, you'll see that they are mostly used to group a bunch of related functions and properties together. Both anonymous object {} and named object XXX {} are used for that. A property of "having a type" is truly secondary and rarely used.

Btw, I've also added a note on "local package" alternative for static objects to the text. It was on the table at some point, but I forgot to mention it.

JavierSegoviaCordoba commented 1 year ago

@elizarov but on sealed classes and interfaces, the object branches are more a type than a group of properties or functions.

Peanuuutz commented 1 year ago

When I see an object, no matter if it had static, I would most likely to read it as it is an object, but static completely defeats the purpose of object. value class on the other side, can be used as a normal class. I don't think these two "pairs" act the same.

If I got the choice, I'd rather not having it at all than exchanging performance with confusion.

mcpiroman commented 1 year ago

Given that

We expect a large number of companion objects to be migrated to statics

and

If you grep the usages of object in Kotlin codebase, you'll see that they are mostly used to group a bunch of related functions and properties together

then why

remember that static objects are going to be a fringe, rarely used feature

? Those three statements do not add up to me. Migrating companion object -> static and object -> static object both give the same benefits (reduced footprint) and are very symmetrical, so why is one of them expected to be frequent and the other rare?

mcpiroman commented 1 year ago

How about virtual object? In a sense that you code it like object but it does not actually exist in memory.

pdvrieze commented 1 year ago

Overall a good proposal. I had considered leaning more upon the compiler to make things static if possible (even if in the companion), but then considered that this would not be ABI compatible (except for private members). One aspect I hope is added is proper (private) static members in interfaces (as allowed by the JVM).

I have however one drawback. My experience is that "static" is not a word that is easily understood (by beginners), but I'm not sure what alternative to use either (perhaps type although the lack of consistency in terminology with Java can also be a problem).

mikehearn commented 1 year ago

@elizarov I read the explanation. The logic is sound, yet I can't help feeling dissatisfied with the results. I think the reason it's controversial is that if a beginner asked "what is the difference between static object and just object", you'd have to answer something like "there's no important difference but static object is more efficient". That answer will prompt puzzled looks of the form "then why isn't it always that way" and you have to get into a discussion about backwards compatibility, the difficulty of proving nobody is relying on the edge cases etc.

Also seems likely that style guides will split on the issue - it's so cheap and quick to add one keyword to reduce bytecode bloat and improve performance a bit, that some people will say you should always write static object. Others will say that no, the performance impact is hardly measurable and it's simpler/more consistent with old code to just use object. Others will follow whatever the stdlib does.

This is one of those times that I wish Kotlin had a targetVersion concept. Really, the default should be static object and then if users need it to have an identity, you'd opt in to that. Without any notion of a defaults version though, our beautiful Kotlin is bound to accumulate such warts and wrinkles as it ages. It may be inevitable, logical and the best possible option at the time, but it still feels a little sad. Well, so be it.

dovchinnikov commented 1 year ago

I think the main reason of the static object situation is unwillingness to get rid of companion object concept altogether. If one greps the usages of object in Kotlin codebase, they'll see that they are mostly used to group a bunch of related functions and properties together, so Kotlin, being a pragmatic language, should respect this and make this the default.

If Kotlin had namespaces/statics from the day 0, then companion object would be a weird concept, which has a single unobvious use case: an ability to use a class reference as a singleton instance of another type. I doubt it'd be added then.

interface Key

interface Job {
  companion object : Key
}

fun usage() {
  println(Job) // object of type Key 
}
stantronic commented 1 year ago

One of the features I was hoping the new namespace feature would have was the ability to add new class definitions within a namespace - e.g. class String.Matcher. Is that still a possibility? I can't find any discussion of that and the use of the static keyword may confuse things as it implies overlap with java concept of static class.

Mr-Pine commented 1 year ago

@stantronic Isn't that already possible with nested classes?

SPC-code commented 1 year ago

If Kotlin had namespaces/statics from the day 0, then companion object would be a weird concept, which has a single unobvious use case: an ability to use a class reference as a singleton instance of another type. I doubt it'd be added then.

@dovchinnikov This is not correct. Companions are a powerful concept. They allow to use type itself as a key (just look how it is done in kotlinx-coroutines CoroutineContext keys). More importantly, companion objects could be used in future to implement companion contracts (that companion is forced to implement specific interface). It would close all remaining functionality of type-classes and add much more.