Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlin statics and static extensions #348

Open
elizarov opened this issue Mar 24, 2023 · 126 comments
Open

Kotlin statics and static extensions #348

elizarov opened this issue Mar 24, 2023 · 126 comments

Comments

@elizarov
Copy link
Contributor

elizarov commented Mar 24, 2023

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:

  • Use 🎉 reaction to vote for static sections syntax.
  • Use 🚀 reaction to vote for static modifiers syntax.

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

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

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

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

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)

@Maxr1998
Copy link

Maxr1998 commented Mar 24, 2023

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

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

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

mcpiroman commented Mar 24, 2023

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

  • Symmetry: you can mark both objects and companion objects static, both having the same semantics (and also the same as in KEEP).
  • Symmetry in extensions: both static and regular companion objects 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.
  • Less new concepts in language (static keyword only on objects, no new static blocks).

Cons of 1):

  • You (probably) cannot have both static and not-static object in a class.
  • You (probably) cannot have multiple static companion objects like you can with static sections.
  • static companion object is rather lengthy.
  • static companion object looks kind of like static classes in Java - confusing and boilerplatey

But those cons are solved in option 2):

  • There can be both companion object and companion namespace in a class.
  • You (probably) can have multiple multiple companion namespaces in a class
  • The symmetry is still achieved with namespace and companion namespace
  • You can have both named and companion namespace in a class, the same as you can now have both named and companion objects.
  • Like in 1), KT-11968 is solved by all classes implicitly having an empty companion namespace which can be extended.
  • Extensions are (probably) like in the KEEP - fun MyClass.namespace.foo(). namespace in this context also looks slightly more meaningful than static,

@sandwwraith
Copy link
Member

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

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

@sandwwraith
Copy link
Member

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

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

@streetsofboston

Will companion objects be deprecated?

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

@TheBestPessimist
Copy link

TheBestPessimist commented Mar 24, 2023

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

spen37 commented Mar 24, 2023

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

Mr-Pine commented Mar 24, 2023

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

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

kyay10 commented Mar 24, 2023

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

He-Pin commented Mar 24, 2023

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

spen37 commented Mar 24, 2023

@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 😆

@udalov udalov changed the title Kotiln statics and static extensions Kotlin statics and static extensions Mar 24, 2023
@mcpiroman
Copy link

mcpiroman commented Mar 24, 2023

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?

@sandwwraith
Copy link
Member

sandwwraith commented Mar 24, 2023

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

Mr-Pine commented Mar 24, 2023

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

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

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

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

rnett commented Mar 24, 2023

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

edrd-f commented Mar 24, 2023

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

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

@edrd-f
Copy link

edrd-f commented May 29, 2023

@Peanuuutz Actually, I think this definition depends on whether the design will adopt the "companion namespace" idea, where conceptually inner namespaces (i.e. inside a class) and external namespaces are treated as an entity, just like object/companion object, or if the current proposal will be kept. If going with the companion namespaces idea, I agree with you, though replacing in your example the inner namespace with companion namespace. On the other hand, if the design stays on the line of Java/JS/etc statics, a modifier section would be inconsistent with other modifiers. The point is that, intuitively, namespace shouldn't be a modifier but an entity like object/companion object, and that gets rid of the whole modifier vs. section discussion.

@elect86
Copy link

elect86 commented Jun 5, 2023

Why don't both? If I have multiple entries, I'll use the static section, otherwise the modifier

Static sections don't make sense because all other modifiers can't be used with section syntax, so this will lead to an inconsistency. For example, it's impossible to do private { fun foo(...) { } }. Why static (or better, namespace, please) should be different?

True, but we are used to static section in Java, so under this point of view it could make sense

@dovchinnikov
Copy link

static section in Java

In Java it's a block of code, it cannot contain member declarations

@Peanuuutz
Copy link

Static section is useful for distinction between static members and non-static ones, and just a few keyword changes from companion object. Yes it writes a little bit more, but I doubt that is a problem actually. Unlike companion object, where you could add extra annotations on it, static section is just a bare block, which means for IDE it's fine to automatically add parentheses and move into it.

The only real mess I'd point out is it differs from Java, where static block is for initialization of static members. This is another place I found more reasonable to use namespace instead. At least it doesn't clash with former experience, while keeping low on understanding difficulty.

@2001zhaozhao
Copy link

2001zhaozhao commented Aug 8, 2023

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.

So, you've picked the name "static" because it is the one used in other programming languages, yet you consider it OK to have a static block that works completely differently from other languages? I think this is even more confusing than just using the term namespace.

From my perspective, there are three good options:

  1. Go with the static modifiers proposal. Simply add the static keyword to functions and properties like other programming languages, and also add a static block which means the same as in Java (a block of code meant to initialize static properties).
  2. Go with static sections, but call it namespace, and accordingly make extensions have the syntax MyClass.namespace.myFunction() rather than MyClass.static.myFunction().
  3. Go with static sections but call it static companion object {} rather than static {}. This disallows creation of multiple static blocks or mixing statics and companion objects, and so maintains Kotlin designers' original intent of grouping all "static" members together, but is less flexible and now mainly serves as a performance optimization and fix to static extensions on Java classes, and little else.

I personally would like to see both (1) and (3). That is, the addition of static modifier to functions and properties and also the ability to define static companion object rather than a regular companion object, representing a companion object with no identity and only static fields/methods (purely for performance purposes; the only semantics that change when you declare a "static companion object" is when calling the Kotlin code from Java, as all methods are now static).

This gives people the choice between statics and companion objects. The companion object syntax doesn't change except for the new performance-optimizing keyword. So, existing Kotlin developers are happy, and Java developers coming to Kotlin are also happy.

Among the feature barriers preventing my Java team to switch to Kotlin, statics are probably the biggest offender - some Java frameworks (especially logging) are a lot more complicated to use in Kotlin due to the lack of statics.

For example, currently to introduce a logger in the Java style, you have to declare it in a companion object. In a smaller class this involves adding 3 lines, while in a bigger class this involves finding where the companion object is already in the code and declaring the logger there, but this is hard to find compared to the Java style of always declaring the logger at the top of the class.

Java:

public class MyClass {
    static Logger logger = LoggingFramework.getLogger(MyClass.class);
}

Kotlin:

class MyClass {
    // Possibly other code that the companion is buried below
    companion object {
        val logger = LoggingFramework.getLogger(MyClass::class.java)
        // Other companion code
    }
}

Even "idiomatic" Kotlin logging frameworks are not ideal as they require you to declare the logger outside the class definition - this means above the class's KDoc documentation and thus in the same lump of code as the import statements. Not where you expect a logger definition to be. Plus, you can't use different loggers between multiple classes in the same file.

import some.Imports // import statements here
val logger = LoggingFramework.logger {}

/**
 * long KDoc
 */
class MyClass {
    // Code
}

Adding the static modifier in Kotlin would solve the issue completely and allow definition of loggers in a convenient way:

class MyClass {
    static val logger = LoggingFramework.logger {}
}

Another use case for static modifier is when you need a helper constant for just a single method and you want to put it together with the method. It doesn't make sense to put it in the companion object which might be far away from the method definition.

class CustomDate {
    private static val weekDays = listOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
    fun getDayInWeek(): String = weekDays[ /* code that depends on this instance */ ]
}

Another example use case with lazy:

class MyClass {
    private static expensiveComputedObject by lazy { doCompute() }
    fun onlyFunctionThatUsesExpensiveObject {
        doSomething(expensiveComputedObject)
    }
}

So in conclusion, I think static modifiers absolutely have some irreplaceable use cases, and are consistent with other languages, so they should be added to Kotlin. Then, to cover the use case of a "more efficient companion object", I think the keyword static companion object should also be added, which does not change the semantics of the companion object except for Java interop.

If the static sections are added instead, then they should be called namespace rather than static (IMO this reduces confusion rather than adding confusion).

@mcpiroman
Copy link

I would too like to see a similar unifocation, that everything inside the brackets is a part of the definition. E.g. variables are a part of a frame which the function defines and properties are a part of an object which the class defines. If it's just something statically hosted in the parent's namespace, define it as such with 'namespace' (or similar) keyword/scope.

It could also allow to do it inside functions:

fun verify(value: String): Boolean {
   //...
   namespace {
      val expr = Regex("...") // cached instance of compiled regex
   }
   return expr.matches(value)
}

@fvasco
Copy link

fvasco commented Aug 20, 2023

@Peanuuutz
I think that package is already a good place to define types instead of namespace.
Moreover deprecating inner is not a backward compatible change.
Finally, I think that the use of inner class should be discouraged, so a longer and explicit syntax should be preferred.

@Peanuuutz
Copy link

Yeah I know that. It's impossible for Kotlin to change how nested/inner class works nowadays. 😄

@CLOVIS-AI
Copy link

CLOVIS-AI commented Aug 21, 2023

@mcpiroman:
It could also allow to do it inside functions:

I'm against this. This is adding global state implicitly, which, as we know from C's static block, often makes code much harder to reason about and test. Global state should always be obvious to the eye, so it cannot be declared inside a function.

@ilya-g
Copy link
Member

ilya-g commented Sep 1, 2023

@CLOVIS-AI
This is adding global state implicitly, which, as we know from C's static block, often makes code much harder to reason about and test.

Did you mean global state in general or global mutable state? In the example given by @mcpiroman, expr is of type Regex which is an immutable type.

@Peanuuutz
Copy link

Peanuuutz commented Sep 1, 2023

Even immutable values should be declared outside a function, because function itself shouldn't contain any persistent state (aka, not functional). It is the responsibility of a class/object to hold those (for top level properties, the holder is the package).

@mcpiroman
Copy link

Even immutable values should be declared outside a function

Do you mean you always write

private const val acceptedExtension = ".txt"
fun isTextFile(name: String): Boolean {
   // check exists...
   return name.endsWith(acceptedExtension)
}

instead of

fun isTextFile(name: String): Boolean {
   // check exists...
   return name.endsWith(".txt")
}

?

String objects are a state. Regex is just some more sophisticated variation over a String. Even primitives would count as a state, and I think at least for those it is fine to declare them inside functions.

You are right that mutable global state declared inside functions would be a problem. Well, mutable global state declared anywhere is a problem. I mostly meant this feature for rather small, temporary constant data which does require allocation or initialization, such as ext in setOf(".png", ".jpg", ...) or Regex in the example above.

But this is quite a different feature than static extensions. I have some more points to back it up, but maybe its better do discuss it elsewhere?

@lukellmann
Copy link
Contributor

@mcpiroman:
It could also allow to do it inside functions:

I'm against this. This is adding global state implicitly, which, as we know from C's static block, often makes code much harder to reason about and test. Global state should always be obvious to the eye, so it cannot be declared inside a function.

To prevent that this kind of global state can be added in functions, statics should be forbidden in local classes (local singleton object declarations and companion objects for local classes already seem to be forbidden).

@SPC-code
Copy link

Let me summarize my impression from this discussion.

I like the idea of namespace/companion namespace the most. There are several reasons for that:

  • I do not like features that solve only one problem. Namespace concept not only allows to introduce static-like blocks, but also allows to add top-level namespaces and nested namespaces. Namespaces are quite important to solve another Kotlin problem - scope pollution. Scope pollution exist not only on the global level, but sometimes on the type-name level as well and namespaces that could be attached to a name of a type solve it.
  • Namespace is a whole concept. You do not need nasty constructs like a "class method that does not belong to a class" to explain it. It forms a clear mental model. And, as I always say, mental models are important for understanding and learning.
  • companion namespace looks similar enough to companion object and would allow seamless migration both in manual and automatic mode. Also, this similarity is helpful to understand the relation between companion object and companion namespace. Actually, it seems like any object inherits all features of a namespace, so we can say that object is a subspecies of a namespace. It looks easy to understand and use: object = namespace + type. It is much more clear than regular `class with a single instance".
  • I like the idea of namespace growing larger than a class. For example, we have packages. Packages are also namespaces. So, if we have tools to work with namespaces, we will automatically have additional tools to work with packages. So, packages are also subspecies of a namespace. The simple example is notorious package private if we will have something like internal(package) in future, it would be much more universal to have internal(namespace) and cover all problems with one solution.
  • I do not find an argument about static being used in Java compelling. Different languages have different words. Namespaces are used in a lot of places. I think that we need to choose terminology for simplicity and usefulness, not for javism.
  • I do not like the idea of single line static because many single line statics in a single class break readability if they are not grouped. But if this is important, probably we can create a shortcut like companion const val a = 22. This would mean that the value belongs to a companion namespace (even if it is an object). And we can do that without introducing another keyword.

@Peanuuutz
Copy link

Peanuuutz commented Sep 26, 2023

Do you mean you always write

private const val acceptedExtension = ".txt"
fun isTextFile(name: String): Boolean {
   // check exists...
   return name.endsWith(acceptedExtension)
}

instead of ...

The problem here is not whether you store something, but where you store them. Functions are not supposed to be this where. That's what I want to say.

Even if we went this path, how could you avoid bad practice like having a persistent mutable list? I'm pretty sure it would be a ktlint rule immediately. If you only allow const val, then I'd say it's just too limited to be useful. It's not worth it.

@DanielGolan-mc
Copy link

DanielGolan-mc commented Oct 13, 2023

I would keep in mind what namespaces are to the average user -- inline objects.

We're basically inlining objects that don't need a type.

Keeping this in mind, in my honest opinion, namespace just sounds and looks much better (although it may be less intuitive).

inline object Kointer
// vs
namespace Kointer

class Kointer {
    inline companion object
    // vs
    companion namespace
}

Additionally, I would support restricting namespaces to only support const vals, but the top level supports non-const properties, so it makes no sense at all.


And lastly, if we choose the namespace keyword, empty namespaces should be disallowed, and non-empty namespaces should be disallowed when not using packages, as we don't want Kotlin to turn into .NET.

// <missing package>

namespace MyLibrary { } // error, namespaces must not be used as packages

package org.example.mylibrary

namespace Utilities // error, useless empty namespace, does nothing

namespace Utils { } // error, useless empty namespace

namespace Util { // ok
   fun meow() = TODO()
}

(also, what a small world, the library I'm using just mentioned this issue while I'm commenting on it)

@Peanuuutz
Copy link

Peanuuutz commented Oct 13, 2023

To me namespace is much more clear than "inline object". For most of the time, when I'm thinking to have an "object", especially companion object, I actually mean to have a namespace, or companion namespace, so that something is shared (not instance based) in a more encapsulated (not top level) way.

Namespaces must be able to declare non-const members. It's not a compile time concept.

Note that you could have extensions on namespaces. Empty namespaces may act like a semantic scope.

@thumannw
Copy link

thumannw commented Jan 5, 2024

The most interesting part for me is still the concept of a static interface. Primarily intended to allow something like this

static interface Creatable<S : Creatable<S>> {
    fun create(): S
}
fun <S, reified T : Creatable<S>> foo(): S = T.create()

(see KT-49392), it could also be used to cover other features from the proposal.

First of all, it could be used as a namespace, replacing static object. Methods with default implementations in static interfaces could be resolved statically.

static interface Delegates {
    fun <T : Any> notNull(): ReadWriteProperty<Any?, T> = ...
}
val property: String by Delegates.notNull()

Related: KT-16900

Next, static interfaces could be used to declare static members of classes, without the need to put the members into static blocks or annotate them with static: A class member is compiled to a static member if and only if it overrides a member from a static interface.

static interface MyStatics {
    fun foo()
}
class MyClass : MyStatics {
    override fun foo() { ... } // this is static
    fun bar() { ... } // this is not static
}

This would only work for public members of the class.

For private members, another mechanism could apply: Any private member of a class is compiled to a static member if and only if it has no reference to this, or in other words, if and only if it can be static. This is basically an optimization, as most of the code would not be affected by this change, unless such members are (unnecessarily) accessed via this or in nested classes via an injected instance. Nonetheless, sure this could only be introduced in Kotlin versions allowing breaking changes.

@zhelenskiy
Copy link
Contributor

@thumannw I think it is better to add static modifier for such interface members instead. This way you would be able to have both static and non-static members in the same interface. Also, you might like self types.

@kegt
Copy link

kegt commented Jan 9, 2024

At first I thought namespace was better than static just like most people,
but after more thought I'm leaning toward the conclusion that we should have both,
because they are actually different things.

Here is why:

  1. namespace has an important feature: it can be declared multiple times, maybe in different files (more like package); the proposed static object syntax didn't imply that for me.
  2. static looks & reads like a modifier, just like public or inline. In the static section syntax, it's used as a language structure name, like object, while namespace blocks make perfect sense.
  3. I think the actual modifier involved here is companion, and companion namespace makes more sense and symmetric with companion object
  4. But the init block looks more weird under namespace
  5. namespace interface is also more weird than static interface
  6. I tend to think namespace is a more high-level concept, so a namespace fun or namespace {} nested in class seems a bit unnatural
  7. C#, TS have namespace and static, Rust have mod

My proposal

static object Foobar {} // ERROR, not supported

// other.kt
namespace foo {
    val someValue = 0
}

namespace foo {
    namespace bar {} // nested namespace
    val answer = 42 // namespace level decl.

    init {} // ERROR: not allowed

    class Color(val raw: Int) { 
        companion {
            init {} // OK
            fun fromString(): Color // factory fun
        }

        companion val Black = 0xff000000 // some 'static' decl.

        companion object {
            ...
        }
    }
}

PS: another syntax I have considered is using Self, but I think it will conflict with the self type propsoal:

class Color {
    Self {
        init {}
    }
    val Self::Black = 0xff000000
    // companion object becomes:
    object Self {}
}

Static extension syntax

val Class.static.ext is inconsiitent with current val Class.Companion.ext syntax, they should be unified.

  1. val Class::object.ext and val Class::static.ext, mimic Class::class syntax
  2. val object(Class).ext and val static(Class).ext, mimic context(A, B)

@thumannw
Copy link

thumannw commented Jan 9, 2024

@zhelenskiy
My idea's intention in fact was to somehow avoid the introduction of static on the callable level. But if that's already decided, it is obsolete.
Let me just share some experience I've made lately when migrating a medium sized Groovy/Java code base to Kotlin. It is quite common to extract code private helper functions in classes, which then may or may not have a reference to this. By default in Java/Groovy there is an IntelliJ inspection suggesting to make such methods static. In Kotlin, I assume, the unused "this" is simply accepted. This will probably change when statics are introduced. Then, if there is only the possibility to collect them in static blocks, there will either be a lot of them, or extracted helper functions will be disconnected from their users, which will probably be awkward during development. Hence this would be an argument for static modifiers instead of blocks.

@Peanuuutz
Copy link

May I ask that when this comes out, will there be code style migration or preference towards List.of, Sequence.generate over listOf, generateSequence?

@revonateB0T
Copy link

Hi guys, what about something like static expressions?
A static expression is evaluated once, and the expr cannot capture variable on function stack.
static expr can be used for collection literals
foo(static { listOf(1, 2, 3)})
Or static local val
val somethingStatic = static { expr }
Or just static memorized function
static { { p -> calculate(p)}.memorize() }

@SPC-code
Copy link

SPC-code commented Sep 6, 2024

@revonateB0T Your proposal is unrelated to the topic. Please refer to https://youtrack.jetbrains.com/issue/KT-14652

@revonateB0T
Copy link

revonateB0T commented Sep 6, 2024

@revonateB0T Your proposal is unrelated to the topic. Please refer to https://youtrack.jetbrains.com/issue/KT-14652

Static expr is not always computed in compile-time.
For example, we have some side effect in static-expr(certain it's discouraged usage)
val unit: Unit = static { mutateSomething() }
the unit is a function local variable, and mutateSomething only execute once regardless the function called any times.

@SPC-code
Copy link

SPC-code commented Sep 6, 2024

@revonateB0T you can easily create complie-time memoization on a library level. And is still have nothing to do with the discussed topic.

By the way, this confusion is additional argument to avoid word static in favor of namespace.

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

No branches or pull requests