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

Inline classes #104

Open
zarechenskiy opened this issue Apr 4, 2018 · 192 comments
Open

Inline classes #104

zarechenskiy opened this issue Apr 4, 2018 · 192 comments

Comments

@zarechenskiy
Copy link
Member

zarechenskiy commented Apr 4, 2018

Discussion of the proposal:
https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md

@gabrielhuff
Copy link

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?

@LouisCAD
Copy link
Contributor

LouisCAD commented Apr 5, 2018

There was a discussion about allowing inline sealed class hierarchies in #103 (comment).

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?

@zarechenskiy
Copy link
Member Author

@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

@zarechenskiy
Copy link
Member Author

@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

@udalov
Copy link
Member

udalov commented Apr 5, 2018

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.

@zarechenskiy
Copy link
Member Author

@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

@gildor
Copy link
Contributor

gildor commented Apr 9, 2018

I think @InlineOnly semantics would be very helpful. you don't worry about an accident boxing and always has close to zero overhead.

@artem-zinnatullin
Copy link

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?

@sakno
Copy link

sakno commented Apr 11, 2018

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.

@RaisedByTheInternet
Copy link

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?

@gildor
Copy link
Contributor

gildor commented Apr 16, 2018

@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

@zarechenskiy
Copy link
Member Author

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

@zarechenskiy
Copy link
Member Author

@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

@zarechenskiy
Copy link
Member Author

zarechenskiy commented Apr 16, 2018

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

@fvasco
Copy link

fvasco commented Apr 16, 2018

inline sealed class is union type for Kotlin

In such case we should consider also inline object Baz : Base()

@LouisCAD
Copy link
Contributor

@fvasco What's the point of Baz in your snippet? Do you have a use case?

@fvasco
Copy link

fvasco commented Apr 16, 2018

@RaisedByTheInternet
Copy link

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

@fvasco
Copy link

fvasco commented Apr 16, 2018

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?

@zarechenskiy
Copy link
Member Author

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

@RaisedByTheInternet
Copy link

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

@fvasco
Copy link

fvasco commented Apr 17, 2018

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 = ...

  }
}

@LouisCAD
Copy link
Contributor

LouisCAD commented Apr 17, 2018

@fvasco Your second snippet had a wrong class name
The use case example is a good one regardless 👍

@RaisedByTheInternet
Copy link

RaisedByTheInternet commented Apr 17, 2018

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

@JakeWharton
Copy link

That isn't a problem with this proposal. All Kotlin behaves like that.

@ricmf
Copy link

ricmf commented Apr 17, 2018

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.

@artem-zinnatullin
Copy link

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

@ricmf
Copy link

ricmf commented Apr 17, 2018

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)

@LouisCAD
Copy link
Contributor

@trevorhackman Because they are still experimental and not all use cases are supported yet. There's an open issue here for that feature: https://youtrack.jetbrains.com/issue/KT-25915

@reitzig
Copy link

reitzig commented Oct 14, 2020

Why is it that inline classes permit only a single property? Is it a "for now" restriction because it's easier to implement (which I believe), or are there fundamental problems with admitting multiple properties?

@janvladimirmostert
Copy link

janvladimirmostert commented Oct 14, 2020 via email

@elect86
Copy link

elect86 commented Oct 14, 2020

@janvladimirmostert which talk are you talking about exactly?

@LouisCAD
Copy link
Contributor

Kotlin Online Event (not KotlinConf @janvladimirmostert), the one named "A Look Into the Future" by Roman Elizarov: https://youtu.be/0FF19HJDqMo

@nschwermann
Copy link

It would be great if we use the inlined class without having to access the internal value, like a delegate.
Example:

inline class Password(val value : String)
val length = Password("abc123_secure!!!").length 
//rather than
val length = Password("abc123_secure!!!").value.length 

@elizarov
Copy link
Contributor

elizarov commented Dec 2, 2020

It would be great if we use the inlined class without having to access the internal value, like a delegate.

@nschwermann Can you elaborate on how that's going to be different from a typealias Password = String?

@fvasco
Copy link

fvasco commented Dec 2, 2020

@nschwermann, are you suggesting that an accidental Password("abc123_secure!!!").toString() should reveal the actual secret?

How handle unit plus uint?
UInt is a wrapper of Int, therefore should be translated to int plus int, with a Int result, or should be translated to int plus uint, so a compiler error?

@nschwermann
Copy link

nschwermann commented Dec 2, 2020 via email

@quickstep24
Copy link

@nschwermann
I think you want

inline class Password(val value : String): CharSequence by value

There is already an issue for that KT-27435

@nschwermann
Copy link

Thanks @quickstep24 yes that is exactly what I am talking about, but I don't think you should even have to use the delegate syntax. Should be automatic.

@reitzig
Copy link

reitzig commented Dec 2, 2020

FWIW, I disagree with that. An automatism would preclude us from ever having incline classes for multiple values, which I'm more interested in than avoiding the few symbols to make delegation explicit.

Never mind that delegation requires an interface (or does it?) which you don't get from a field with a class type.

@sollecitom
Copy link

Also would prevent us from encapsulating / hiding functions or values we do not want to expose. Allowing to implement an interface by delegating to the actual value would be great and probably enough.

@nschwermann
Copy link

nschwermann commented Dec 2, 2020

@reitzig ticket linked says they are considering allowing the delegate syntax even when its not an interface or open class. I don't think inline class with multiple values is even on the table. What would be the point of that? No longer could compiler replace the inlined class with actual implmentation if there were multiple values.

@sollecitom giving the auto delegate doesn't solve the problem of hiding implmentation of inlined class. You can still can (but have to) call .value and do anything you want to the inlined class. In that case, better to just have a privately backed class.

That gives me an idea! If you make inline class value public you get this feature, if its private you dont!

@quickstep24
Copy link

@nschwermann
You NEED the delegate syntax because an inline type is NOT (always) the same as the wrapped type.
There are at least four usages/benefits for inline types:

  • readability - use inline classes like a typealias for clarity in code
  • extension - the inline class provides additional methods but otherwise mirrors the wrapped type (you could use extension functions instead)
  • type safety - the inline class generates a new type for an otherwise identical structure - the Password example falls into this category, but also Id<T>(val id: int)
  • performance - the inline class provides pass-by-value behavior for simple, small classes with a single attribute, UInt and most of the other inline classes in stdlib fall into this class

Case 1 you should use typealias instead
Case 2 you would benefit from implicit inheritence
Case 3 depends on the actual use case, for example you wouldn't want id = Id(1) + Id(2) to compile
Case 4 you usually do not want implicit inheritence

Tying inheritence to visibility would mean mixing two otherwise unrelated aspects.
If you want delegation behaviour, you should have to declare it.

Moreover, the signature of 'inherited' methods is completely unclear.
What is Password("Hello") + Password("World")? A String or a Password?
And how about Password("Hello") + "World"?
Should Password("Hello") + User("World") compile?
And if it does, where did our type safety go, which is why we introduced Password in first place?

@nschwermann
Copy link

@quickstep24 you are missing my point. My only complaint is you should not go through the burden of inlinedName.value.apiWanted instead you should always be able to do: inlinedName.apiWanted So password would still be a type password giving you the extra type safety you want just save the extra step of accessing the boxed class over and over again.

Still Password("Hello") + User("World") would not work. In that case you still need to use the .value BUT: You should be able to do val length = Password("Hello").length I'm sure this is a hard compiler problem to solve but it would sure be nice.

@quickstep24
Copy link

@nschwermann you are right, I do not get your point. Please rethink on your expectations. Why should Password("Hello").length compile, but Password("Hello") + User("World") not? If Password behaves like it's wrapped String, both will compile.
What you expect is not "hard to solve", it is impossible. The compiler would have to guess what to delegate and what not.

@nschwermann
Copy link

nschwermann commented Dec 3, 2020

@quickstep24 my thoughts are String does not have defined plus operator with User so it should not work. Type should be kept, api of inlined class exposed.

'Password("abc") + User("name")'
//Does not compile can not add User to String user is the argument

It's not a delegate, it's similar to one.

@quickstep24
Copy link

@nschwermann String has public operator fun plus(other: Any?): String

@nschwermann
Copy link

@quickstep24 that is fine, the meta data that the argument is a User not a String should still be intact when the plus method is called with the argument as type Any and the implementation of plus would should treat it as such. In these types of cases it would probably be good to have lint warning which could even be configured as an error when you pass an inlined class to an Any parameter.

@sgrimm
Copy link

sgrimm commented Dec 30, 2020

Is the prohibition on vararg parameters of inline classes likely to be revisited before inline classes come out of beta?

I experimented with using inline classes to wrap Long database identifiers in a jOOQ application to get additional type safety in SQL queries. (Currently I use data classes, which have the usual wrapper-class drawbacks.) But jOOQ's Kotlin code generator emits functions such as

fun fetchById(vararg values: DeviceId): List<Device>

And of course when I change DeviceId from a data class to an inline class, this fails to compile with Forbidden vararg parameter type: DeviceId.

I understand the implementation complexity involving arrays of inline classes makes this a tough problem to solve. If the restriction is going to remain in place, then the right fix will be to change the code generator, since I expect wrapping database IDs will be a pretty common use case for inline classes.

I use jOOQ as an example here but of course the same would apply to any other code generator that can produce functions with vararg parameters of user-defined types.

@Blackdread
Copy link

I wanted to use inline class before for IDs, but since it was still beta and many issues with it, I changed to use data class with single field.
If all constraints around inline class can be solved then I might change again, in any case data class works fine.

@fluidsonic
Copy link

fluidsonic commented Jan 11, 2021

I've used inline classes extensively for more type-safety (e.g. Password, EmailAddress, PhoneNumber, CompanyName, Capacity, etc.) each wrapping a primitive String or Int.

At some point I was annoyed that I couldn't do something like emailAddress.toLowerCase() because I had to unwrap it first. Or add two wrapped Int of the same type. Or compare them using <=. So I thought that it would be nice to have a way to automatically add all functionality of the wrapped type, just with the wrapped type instead. E.g. EmailAddress automatically gets all functions and properties of String like toLowerCase() or length.

After a while however I came to the conclusion that that's not good. Such types typically have values that are narrower than the types they wrap. E.g. all PhoneNumber values are also String values but not all String values are also meaningful PhoneNumber values.

  • For example a PhoneNumber is an entire number but a .substring(…) or .padStart(…) wouldn't return anything meaningful. It doesn't make sense that they return a value of type PhoneNumber.
  • The constructor may impose restrictions on the wrapped value. E.g. a String value passed to PhoneNumber may be checked for valid syntax. That way you can be certain that an PhoneNumber instead always contains a syntactically valid phone number. Existing String functions wouldn't honor that contract.
  • Your inline classes would start to add API to each other. Let's say you have a String.toPhoneNumber() function. If now all inline classes that are based on String and inherit its API would also have that function. E.g. EmailAddress.toPhoneNumber(). Doesn't make sense either.
  • Most functions of String are totally useless for your wrapped type. They confuse rather than help.

The remaining cases can be implemented selectively on your type.

Password is also a good example. I have that type too. It's value property is public and its toString() merely returns •••••• to prevent accidental logging etc. You wouldn't want most String functionality on that type either. But you usually do want to have the underlying String accessible because otherwise you can't do much with the value outside of the system (e.g. serializing it, hashing it, etc.).

It's a little different with numeric types, e.g. an Int-backed inline class. The arithmetic functionality is often desired, as is the comparison of two values. Instead of making all functions of Int available to an inline classes wrapping an Int it makes sense to instead simplify the way arithmetic types are defined. Swift's arithmetic and numeric protocols are a great starting point for that. But that's an entirely different topic.

@elizarov
Copy link
Contributor

@sgrimm Is the prohibition on vararg parameters of inline classes likely to be revisited before inline classes come out of beta?

Unfortunately, no. Support for varargs of inline classes will be added later. They will not be available in the initial release.

@reitzig
Copy link

reitzig commented Jan 11, 2021

Such types typically have values that are narrower than the types they wrap

Good point. I'm not sure if they are legal for inline classes already, but interface conformance by delegation seems to be a decent solution here. Write an interface with the methods you want, delegate the implementation to the wrapped value, done.

@elect86
Copy link

elect86 commented Nov 7, 2022

I don't get the point of inline (non reified) classes.. if the compiler map them to the upper bound, what's the point to prefer them against traditional classes?

@NorbertSandor
Copy link

NorbertSandor commented Nov 7, 2022

what's the point to prefer them against traditional classes

As I know value classes have performance benefits.

(To tell the truth, I think they are a very good and bright idea but imho not worth the amount of work put to developing them by the Jetbrains team...)

@elect86
Copy link

elect86 commented Nov 7, 2022

Not with generics, apparently. If this:

@JvmInline
value class IC<T>(val a: T)

fun foo(ic: IC<Int>)

compiles to:

fun foo-<hash>(ic: Any?)

then the whole purpose to use an inline class fails

@NorbertSandor
Copy link

Not with generics, apparently.

Maybe I don't understand something.
In case of non-value classes the parameter of function foo would be IC<Int>.
In case of value classes it is ic: Any?.
This is the main performance benefit of value classes: hopefully there will be no need to box/unbox the value of a inside an instance of IC object.

What would be a better solution in your opinion?

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