Open elizarov opened 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?
(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
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.
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:
Color_background
Color::background
(:
is an allowed character in JVM unqualified names)static extension Color getBackground
(spaces are allowed in JVM unqualified names)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.
Great proposal!
Few concerns that popup in my head.
Will companion object
s 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 object
s 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
.
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.
Alternative proposals:
static
section have static companion object
, so that it is symmetric with static object
, orstatic object
with namespace
, thus also having companion namespace
(instead of static
section).Pros of 1):
object
s and companion object
s static, both having the same semantics (and also the same as in KEEP).companion object
s can be extended with the same syntax (fun MyClass.Companion.foo()
. KT-11968 is solved by all classes implicitly having an empty static companion object
. It can be then explicitly replaced with user-specified one, either static
or not.static
keyword only on objects, no new static
blocks).Cons of 1):
static
and not-static
object in a class.static
companion objects like you can with static
sections.static companion object
is rather lengthy. static companion object
looks kind of like static class
es in Java - confusing and boilerplatey But those cons are solved in option 2):
companion object
and companion namespace
in a class.companion namespace
s in a classnamespace
and companion namespace
namespace
in a class, the same as you can now have both named and companion objects.companion namespace
which can be extended.fun MyClass.namespace.foo()
. namespace
in this context also looks slightly more meaningful than static
,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 object
s.
I also like MyClass.namespace.something()
much better than MyClass.static.something()
.
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.
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?
@streetsofboston
Will companion objects be deprecated?
It is a non-goal of this proposal to deprecate or to completely replace companion objects.
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.
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?
This would conflict with Extension functions as static members but I agree with you and don't really like fun SomeClass.static.extension()
either
@TheBestPessimist
This works because ::one
and ::two
are property references (KProperty1
s) , which don't have this
reference. As suggested, this is the same as writing Outer::one
(in any scope).
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.
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
@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
π
Notes:
There is no mention on how does this KEEP corelate with KT-45587 Tuples (structural literals and structural types)
List.empty
and List.of
are nice, but if there will really be a migration of listOf()
-like functions, then it should rather be directly to collection literals, lest there are 3 ways equivalent to create a list.
In the example
Option: Static section syntax.
class Color(val rgb: Int) : Parseable<Color> { static { fun parse(s: String): Color { /* impl */ } } }
Shouldn't fun parse
be override
?
@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.
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
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.
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 */ }
}
}
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
}
}
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.
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.
As I like the grouping aspect that automatically came with companions, I highly favor the grouping syntax here. π
Great proposal. I've been waiting for this since I started using Kotlin.
Few observations:
static object
were replaced with namespace
.Question:
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?
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
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() {}
}
}
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.
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.
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 thatgetBackgroundColor
would map toColor.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.
Spaces also don't work on Android except on the absolute newest versions so I would entirely reject that as a possibility.
@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.
@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
.
But
1
is not a type, while anobject
is a type. When I see anobject
, 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.
@elizarov but on sealed classes and interfaces, the object
branches are more a type than a group of properties or functions.
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.
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?
How about virtual object
?
In a sense that you code it like object but it does not actually exist in memory.
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).
@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.
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
}
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.
@stantronic Isn't that already possible with nested classes?
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.
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.