KEEP
KEEP copied to clipboard
Inline classes
Discussion of the proposal: https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md
This is an awesome idea and I can definitely see myself using this feature in different scenarios.
I just have a small question. From an implementation perspective, inline classes feel more like interfaces than actual classes (mainly due to their stateless nature).
The only reason I can think of that justifies the use of classes instead of interfaces is instantiation: instances can be "created" by calling the constructor. But I feel like there could be another mechanism to accomplish this.
What do you think?
There was a discussion about allowing inline sealed class hierarchies in https://github.com/Kotlin/KEEP/pull/103#discussion_r179326199.
Since inline classes support autoboxing just like primitives, this could be possible to use inline sealed classes and do is checks in when or boolean expressions.
However, wouldn't it result in autoboxing most of the time (as is check is not possible on inlined value), defeating the purpose of making them inline?
@gabrielhuff Do you mean that inline classes are similar to something like "newtype" or "strict typealiases"? And they can be used like this:
inline type UInt = Int
fun test() {
var x: UInt = 0
x = 1 // type mismatch!
}
?
It's interesting, but we believe that current form to declare members/types is suited for us better
@LouisCAD Yes, we can imagine inline sealed classes like this:
inline sealed class Base
inline class Foo(val s: String) : Base()
inline class Bar(val i: Int) : Base()
All subclasses must have different runtime underlying representation.
And you're right that in current design autoboxing will occur even on the simplest cases:
fun box(b: Base) {}
fun test() {
box(Foo("")) // boxing
}
Basically, yes, currently this is a major reason to prohibit inline sealed classes for now
I wonder if @InlineOnly inline classes were discussed? That is, classes which are always inlined, and an attempt to box an instance of which would cause a compilation error. In theory, their metadata could be stored more compactly than in the .class file, and the whole .class file is not needed for them at all.
@udalov Interesting! In other case, for @InlineOnly underlying representation could always be used:
@InlineOnly
inline class Name(val s: String)
fun foo(n: Any) {}
...
foo(Name("K") // no boxing
Here .class file also is not needed
I think @InlineOnly semantics would be very helpful. you don't worry about an accident boxing and always has close to zero overhead.
Current spec seems to not cover annotations on inline classes.
It seems that annotations are pretty much useless since class gets erased after compilation.
Moreover they might be even dangerous as they might trick user into thinking that they'll make difference:
inline class UserId(@SerializedName("user_id") private val id: String)
However I guess you could give compiler plugins a chance to introspect them (which might be very useful).
WDYT?
Due to nature of inline class it should be passed by value. If so, this feature intersects with const data class feature and upcoming value types proposed by project Valhalla as part of Java 10.
Since inline classes can't have init blocks, how can we do validation?
For example, suppose I create a Username class:
inline class Username(private val value: String)
And I want to throw an exception at run time if somebody tries to create a Username that contains spaces:
Username("Invalid Username") // error
Will this be possible?
@RaisedByTheInternet This is also true for Data classes.
Maybe private constructor + factory function can help with that because there is no such problem as with data class that also exposes copy method
@artem-zinnatullin Yes, annotations on underlying value should be prohibited, thanks for pointing this out! Moreover, there might be inconsistent behaviour for boxed/unboxed representation.
It's hard to say about compiler plugins for now, could you please elaborate more about use cases?
@sakno As I see, const data classes can use very restrictive set of types as an underlying value to ensure constant hashCode/toString. So, goals and motivation of inline classes are different.
Yes, we're closely watching the project Valhalla and probably will try to use their mechanism for value/inline classes in future
@RaisedByTheInternet No, currently this is impossible. Unfortunately, the current restrictions are made in a such way to avoid any initialisation (validation) for inline classes. Seems that it's impossible to have consistent behaviour and Java interop if we'll allow to have init blocks or private primary constructors.
inline sealed class is union type for Kotlin
In such case we should consider also inline object Baz : Base()
@fvasco What's the point of Baz in your snippet? Do you have a use case?
@LouisCAD https://github.com/Kotlin/kotlinx.coroutines/issues/330
@zarechenskiy
Hmm, if we can't do validation then this feature seems very limited. For example, we have this UInt inline class:
inline class UInt(private val value: Int)
If anyone can simply say UInt(-1) then it almost seems pointless to use an inline class here. Apologies if I'm missing something.
Hi @RaisedByTheInternet
how you define UInt.MAX_VALUE?
The following statement is invalid
const val MAX_VALUE: UInt = UInt(0xFFFF_FFFF)
C has the same issue. Inline classes miss of encapsulation.
Have you some proposal for this feature?
@RaisedByTheInternet You are right and yes, this is one of the most controversial restriction.
As @fvasco mentioned, UInt(-1) will probably represent max value, but of course it would be cool to avoid expressions like UInt(-42)
@zarechenskiy
It sounds like I may have misunderstood, as I didn't realise that UInt(-1) could be represented as the maximum value.
But how will that work? Something like invoke() inside a companion object?
I wish bring to your attention another common use case, I hope that considering a bit more complex problem can help us to understand the simpler one.
I want to define a Point type as (x: Int, y: Int).
I can use a single Long backing field to build my type, ie:
inline class Point private constructor(private val long: Long) {
constructor(x: Int, y: Int): this(wrap(x, y))
val x: Int get() = ...
val y: Int get() = ...
compantion object {
fun wrap(x: Int, y: Int): Long = ...
}
}
In such case, as @gildor said, we have "private constructor + factory function".
In UInt case
inline class UInt private constructor(private val int: Int) {
constructor(value: Long): this(wrap(value))
fun toLong() = ...
compantion object {
fun wrap(value: Long): Int = ...
}
}
@fvasco ~Your second snippet had a wrong class name~ The use case example is a good one regardless 👍
@fvasco:
I see. Thanks for the examples.
I guess one problem with the UInt example is that we need to pass a Long, and so something like this won't work:
val value: Int = getValueFromSomewhere()
UInt(value) // error: a Long is expected and we tried to pass an Int
Passing value.toLong() works, of course, but this looks awkward.
That isn't a problem with this proposal. All Kotlin behaves like that.
Why should annotations be prohibited on properties? In the previous example
inline class UserId(@SerializedName("user_id") private val id: String)
The annotation could simply be inlined as well. This would allow for use with existing APIS/frameworks like JPA, and it seems to me like it is the expected behavior.
@ricmf
The annotation could simply be inlined as well.
Inlined where?
Idea of inline class is that it's only present as a separate type in compilation time. Pretty much how Java generics work in many cases.
ie:
inline class UserId(@SerializedName("user_id") private val id: String)
fun test(userId: UserId) {
}
^ after compilation fun test() will be a function that accepts String, not UserId. There won't be UserId at runtime.
So… where should we put information from annotations? (I mean, you can create a meta registry of inline-classes and then annotate all functions that use them thus allowing runtime reflection, but I doubt it's useful)
It could be inlined on the filed/parameter that resulted from inlining the class.
inline class UserId(@SerializedName("user_id") private val id: String)
fun test(userId: UserId) {
}
class Foo(val userId : UserId)
becomes
fun test(@SerializedName("user_id") id: String) {
}
class Foo(@SerializedName("user_id") val id : String)
Well, yes but:
- Lots of frameworks/libraries won't be looking at annotations on method parameters
- Annotations might not be compatible with method parameters.
@SerializedNameis only allowed on fields and methods for instance. Though Kotlin compiler still can produce bytecode with annotated method parameter. - Inlining annotations to a field can result in unexpected behavior (see below)
inline class UserId(@SerializedName("user_id") private val id: String)
data class Request(
private val userId: UserId,
…
)
Inlining annotation to the field is very questionable:
data class Request(
@SerializedName("user_id") // ???
private val userId: UserId,
…
)
There is a way however to keep some meta-information about inline classes, but I'm not sure we should invest time into that.
Given:
package test
inline class UserId(@SerializedName("user_id") private val id: String)
fun test(userId: UserId) {
}
and stdlib interface:
interface InlineClassMetadata {
val fqn: String
val fieldAnnotations: List<Annotation>
…
}
Compiler can produce following code (pseudo):
package test
enum class InlineClassRegistry(
override val fqn: String,
override val fieldAnnotations: List<Annotation>) : InlineClassMetadata {
UserId(
fqn = "test.UserId",
fieldAnnotations = listOf(object : SerializedName { override fun value() = "user_id" }))
}
@kotlin.Metadata(d1 = "p1.inline_class=test.InlineClassRegistry.UserId")
fun test(userId: String) {
}
Lots of frameworks/libraries won't be looking at annotations on method parameters
That's true, but i don't see how it's an argument for forbidding annotations. If the frameworks don't search for annotations in parameters then nothing happens. If you expected them to, then that's a problem with your understanding of the framework.
Annotations might not be compatible with method parameters.
Then the compiler can ignore them since using them on parameters would be illegal and you should be aware of that when using it in an inline class.
Inlining annotation to the field is very questionable:
What would be the expected behavior in this situation in your opinion? That's exactly how i would use this feature / expect it to behave. It's what JPA's Embeddable is for, for example. Inlining the annotation would allow for zero-cost, arbitrary granularity in your model, even if the data in the database/xml/json is completely flat, and the framework being used wouldn't even have to know about it.