Inline enum proposal


#1

So, this follows a discussion in the inline class KEEP.

Inline class is the cool feature I’m looking forward since some time in order to overcome a couple of Kotlin/jvm limitations.

In the meanwhile, I’d like to suggest a bunch of features which I’d really love:

  • define the i-th enum without passing anything, the default value would be ordinal in that case.
    if you define an enum passing a value, the very next starts from that

  • inline enum class WindowFlag(val i: Int) {
      noBorder, // 0
      noAlpha(4),
      noMove // 5
    }
    
  • sealed inline enum classes. In order to have hierarchical inline enums. This means allowing an inline class to extend only another inline class (of the same value type)

What do you think, guys?


#2

Correct me if I understand you wrong. The keep already contains inline enum so what you are proposing is to add an automatic generation of the integer or long values for inline enums of those types.

I can think of one situation where this might be useful. This could help a lot if you want to create an enum class which values are ordered.

enum class Sorted (val i: Int): Comparable<Sorted> {
    First,
    Second,
    Third;

   fun compareTo(other: Sorted) = i.compareTo(other.i)
}

I have a few questions though?

  1. Why does this require the enum to be inline? Yeah ok, otherwise we could have multiple values for each enum, but we could declare the enumerated integer using an annotation or some other keyword.
  2. Based on my use case there is no reason for skipping values. Skipping values starts to look a lot like magic numbers. If some of them need to have a specific value I think all of them should have their value declared by the programmer.
  3. Are there any use cases I have missed?
  4. How exactly are sealed enum classes supposed to work? Can you give an example for that? Wouldn’t they brake with enumerated values?

Based on that I am not really sure if this is a feature worth adding to Kotlin. I don’t see how this one use case is stronger than the minus 100 points rule.


#3
  1. For two main reasons:

    • Smaller memory footprint
    • Faster bitwise operations

    Basically inline enums class is how enums should have been implemented in the first place…

  2. I have an huge pressure to skip values…
    Working with low-level real time graphics means that I have to deal a lot with c/c++ libs. These libs make a massive usage of int/enum constants.
    Just to name a few:

    they rarely are contiguous and start from 0, forcing me to manually write for each of them the corresponding value…

Sealed enums will be a blessing especially for OpenGL and Vulkan. Later api version usually add enums to the same logic group.
In this way I could easily exploit type safety via a hierarchical structure.
For example, in OpenGL there are commands that are supposed to bind some kind of buffers.
Calls of last OpenGL versions accept a larger number of buffer type. This mean I could have:

inline sealed enum TargetBuffer(open val i: Int) { 

    inline enum TargetBufferA(override val i: Int) : TargetBuffer(i) { first, second, third }
    
    inline enum TargetBufferB(override val i: Int) : TargetBuffer(i) { other, anotherOne }
}

and requesting the corresponding enums when and where it’s appropriate

other cool features I’d love:

  • the possibility for enums to implement a base interface in order to avoid this code overhead per enum

enum class DrawListFlag { AntiAliasedLines, AntiAliasedFill;

val i = 1 shl ordinal

}

infix fun DrawListFlag.or(b: DrawListFlag): DrawListFlags = i or b.i
infix fun Int.or(b: DrawListFlag): DrawListFlags = or(b.i)
infix fun Int.and(b: DrawListFlag): DrawListFlags = and(b.i)
infix fun Int.has(b: DrawListFlag) = (this and b.i) != 0
infix fun Int.hasnt(b: DrawListFlag) = (this and b.i) == 0

  • give the possibility for inline classes to have vars. In this way I could pass an inline primitive/enum reference and modify it from there.

fun getApplicationPropertyBool(appKey: String, property: EVRApplicationProperty, error: KMutableProperty0<EVRApplicationError>? = null): Boolean

and call

var err = EVRApplicationError.None
val prop = getApplicationPropertyBool(appKey, property, ::err)
// check error
  • bitwise operators (the very next natural step)

#4

I don’t agree, being able to have enums with values of all types is pretty useful. I agree that the concept of inline enums is great and I love that it is becoming a part of kotlin but I don’t agree with your statement that it should be the only or default way for enums.

Yeah, I did not think about C/C++ interop.

I still don’t understand how sealed inline enums are supposed to work

sealed inline enum Base(val i: Int) { A, B, C }
inline enum Foo(override val i: Int) : Base(i) { 
   F1, // i guess i = 3
   F2, // i = 4
   F3 // i = 5
}
inline enum Bar(override val i: Int): Base(i) {
  B1, // i also 3 // this conflicts with Foo
}

The KEEP currently only allows for inline classes with a single val. As enums are classes in kotlin this also applies to inline enums. And I don’t think this is going to change because immutability is a requirement of a primitive. You can not change the value of 4 to something else. You can only change a variable, which is set to 4 to something else. Same is true for inline classes.


#5

I never meant to limit inline classes to integers, dude :upside_down_face:

Yeah sorry, the sealed inline enums example was broken, I should have fixed it now

Inline classes will be directly translated by the compiler, so this

var a = Error.None

will be written under the hood as

var a = 0

Now, If I pass it to a method as a reference I’m actually able to modify it. (KMutableProperty0<Int>)

I cannot see any reason why this shall not be applied to inline enum as well


#6

Maybe I’m confused, but I’d love to see an example where you pass an int to a function and have that int modified.

var i: Int = 0
foo(i)
// i will still be 0 here. No matter what foo does

Have you, because I still don’t understand how you resolve the values for the inheriting classes. Or are you fine with them overlapping? If I look at your example:

first == 0
second == 1
third == 2
other == 0
annotehrOne == 1

This however is wrong IMO. I think all values of TargetBuffer need to have different values for i but they don’t in your example. Or do you think that other should be 3? This is also a problem, because in bigger files you don’t see at first glance that i is not starting at 0.


#7

Maybe I’m confused, but I’d love to see an example where you pass an int to a function and have that int modified.

Kotlin is really cool, you can actually use a delegate to take the reference and use it as a local variable…!

Have you, because I still don’t understand how you resolve the values for the inheriting classes. Or are you fine with them overlapping? If I look at your example:

They’ll have for sure different values, that was a basic example, but you need to imagine it passing GL_UNIFORM_BUFFER and similar to constructors


#8

Nice, I did not know that was possible. But I guess this would still work even if the value inside of the inline class is a val.


#9

Uhm, this is super interesting. Maybe some JB guy can answer already that


#10

I just tested it

operator fun <R> KMutableProperty0<R>.setValue(host: Any?, property: KProperty<*>, value: R) = set(value)
operator fun <R> KMutableProperty0<R>.getValue(host: Any?, property: KProperty<*>): R = get()

inline class Foo(val i: Int)
var f = Foo(4)
fun modify(ref: KMutableProperty0<Foo>) {
	var a by ref
	a = Foo(1)
}
fun main(vararg args: String){
	modify(::f)
	println(f.i)
}

Sadly it crashes with this error

Exception in thread “main” java.lang.ClassCastException: tests.lwjgl.Foo cannot be cast to java.lang.Number
at tests.lwjgl.HelloWorldKt$main$1.set(HelloWorld.kt:161)
at tests.lwjgl.HelloWorldKt.setValue(HelloWorld.kt:147)
at tests.lwjgl.HelloWorldKt.modify(HelloWorld.kt:156)
at tests.lwjgl.HelloWorldKt.main(HelloWorld.kt:161)

I am however using an eap build and inline classes are experimental, so I believe this is due to the fact that there are still bugs in inline classes.

PS: if anyone wonders about the line numbers, I just threw this code into my current project which is set up already for inline classes. I did not want to go through the effort of looking up the gradlew setup again :wink:

Edit: It looks like there is already an issue about this https://youtrack.jetbrains.com/issue/KT-25325 and it will be fixed in 1.2.7