-
Notifications
You must be signed in to change notification settings - Fork 363
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
Design Notes on Kotlin Value Classes #237
Comments
Yesterday we had a long discussion in kotlin telegram chat about that. There was a nice proposal there that should be at least discussed. The idea is to be able to isolate all value class mutations into a single mutating lambda function. It could be done in two ways: either allow mutations only in a mutating lambda/function with an appropriate receiver or allow those lambdas alongside simple mutations (one can also allow only lambdas at first and relax the rule later). There are two practical benefits of such lambdas:
Also, I have an additional educational point in favor of isolation - it is a mental model. The mental model explained in the design notes is understandable from the point of view of an experienced programmer, but there are a lot of concepts there. All those mutating modifieds, At the end of the design notes document, there is a short discussion about non-constructors for data classes (the idea is really nice, but I think it should be discussed more). Those non-constructors are in fact follow the same idea of mutating lambda, so having them both would probably also simplify the mental model. |
Update on the
|
Please sorry for my trivial question, but I missed something important. If a All other properties are really similar to regular properties without backing field. If so, the
or
It looks really similar to a regular class
We really need a new modifier? |
Consider, for example, an immutable type for complex numbers with properties Regardless of "backed by a field or not", from all these properties,
Now, having read this, you can say that Kotlin already has a feature to distinguish these two kinds of properties on immutable classes. In Kotlin, we can define an immutable
That is, we can distinguish different kinds of properties by whether they are constructor properties or body properties. Constructor
|
I guess the value classes should just be sort of literals (because they really are not allocating memory on the heap, like another wrapper classes) and hence fully immutable. (Correct me if I'm wrong) Also written in KEEP:
And to extend/shrink modulus of a complex number there can be another function to take the components, form a new complex and return that. Edit: Also wanna add, that there could be a annotation which can generate a compile-time |
Hi, @elizarov, My question is: how many modifier are allowed for Function's parameters are Sincerely, I didn't see the point to add some kind of wither modifier. I made a trivial example, just to understand better the issue.
How this declaration differs from:
If any value class is identified by its state, I don't need to update an unmodifiable type, I have to create a new value with arguments, and it is always possible to create a new value with different values (please correct me if I wrong). I really see a
How differs these two variable? |
@fvasco The difference is syntactic. The goal is to enable working with immutable classes just like with mutable types. Indeed, every time you "modify" an immutable class you create a new value. We want the syntax for this operation to be as concise as for mutable classes so that people who choose to model their domain with immutable classes don't suffer from the added boilerplate. Now, assume I want to modify a
I want to write this, just like I do it for mutable classes:
The example is not that great for complex numbers, though. The actual design notes have a more striking example where the syntactic difference is quite apparent in the section on deep mutations. |
I agree with you, @elizarov. I view a strict analogy from base classes (Int, Char, ...) and value classes: both miss of identity and both are immutable. Considering your example above, in my view it is possible to update a variable if it is declared as a So:
Regarding the deep mutation, assuming that all types are value classes, the statement |
Some general feedback: value classes for the purpose of optimizations look great. Often it's necessary to model domain objects like On the other hand, the whole "immutable way of working with mutability" (i.e. creating new instances when assigning new values for Using I understand Another point is how compiler plugins could be used to solve some of the mentioned issues. For example, the following: order = order.copy(
delivery = order.delivery.copy(
status = order.delivery.status.copy(
message = updatedMessage
)
)
) could be val updatedOrder = order.deepCopy {
delivery.status.message.mutate(newValue = updatedMessage)
} where |
@fvasco Continuing from the other thread: On potentially making val/var superfluous and removing them altogether, there would be a few things to consider:
I’m sure there’s a lot more, but it is still interesting to consider. More generally, I notice the comparison of value types to primitives occurring frequently, but I have to wonder how useful that analogy really is. In a theoretical sense it may be nice, but the “law of leaky abstractions” probably applies here as well. We are adding a lot of functionality to what was previously just the set of primitives after all, and the list of supported features can only grow. I also have to wonder if Valhalla won’t be petitioned to add normal field setters at some point in the future - assuming it’s even possible. The desire to focus on immutability rather than just emulate C# structs always struck me as odd, though I’m sure they have good reason for it. Carving out space for this in Kotlin might not be the worst idea, especially for Kotlin/Native. Finally, on the case of non-mutating custom setters: I’m not sure what the “mutating” keyword would be, but my vote is for option 2 suggested by @elizarov, where the keyword is only needed on the non-mutating case. However, what if we moved the keyword to the setter itself?:
By doing so, |
This issue should be addressed in the
Nothing new. class Sum(a: Int, b: Int) {
val n = a + b
} Yes, updating the parser is an issue, but we are considering changing the language.
I will vote no for this proposal. |
With my proposal, switching from
to
results in a compilation error. Instead, switching from immutable to mutable leads to an errors' party. |
Everything can be solved by a plugin. The whole idea of having better support for immutability is to have this plugin built-in into the language itself.
This is similar to the concerns that @altavir shared above, so let me answer both here. The argument is that these "implicit copies" are spooky, hard to see without IDE, etc, so let's have some more verbose, more explicit, scoped syntax for them. We could invent lots of different approaches to this "more explicit syntax" (the quote above shows just one). These concerns are valid, yet, regardless of how this "more explicit syntax" will look like, they gloss over the key fact: Mutations (creating new copies) of immutable types are safe (they cannot have side-effects on unrelated parts of the system), while mutations of mutable types are dangerous (they can accidentally affect unrelated code that kept a reference to the same instance). It is wrong to design a language in such a way that performing a safe and mostly harmless operation (like creating a new immutable value) requires more boilerplate than performing a more dangerous operation (like updating a field in a mutable instance) that requires a lot of attention and forethought from a programmer. Indeed, if those two kinds of mutations look syntactically the same, then changing a (safe) immutable value class to a (dangerous) mutable class will cause the old code to still compile, yet it will start producing weird bugs. Let's see how this problem can be solved. Both @altavir and @edrd-f propose to distinguish (syntactically or contextually) these two kinds of mutations. However, taking into account the key fact, it means that we have to make (dangerous) mutations of mutable classes more explicit, not vice versa. The safer operation should not be more verbose than a more dangerous one. There is a simpler solution to this problem. First of all, note that the problem is not novel. It is happening right now all the time. People make mistakes of using mutable classes where they should be using immutable ones. As you write, it compiles "with no errors or warnings, however, all sorts of bugs would appear." However, as we improve support for immutable classes in the language (make it less burdensome to use immutable values) we can start adding mechanisms to require immutability in various contexts (like asynchronous data pipelines) or at least warn on attempts to use mutable data there. Now if you accidentally use a mutable class where you should have been using an immutable one then you'll get, at least, warned. |
Wither is not needed for
|
Would custom setters be called for each nested “wither” in the example? That would explain the motivation for disallowing them. However, the case I’m interested in is the one lacking a backing field, which I suppose wouldn’t be touched being just functions, essentially. |
Yes @elizarov, I agree. But I suppose to argue about new modifiers (#237 (comment)), not about implementation details. |
I think it's fair to mention here the partially related issue of KT-44530. The quick TL;DR is that currently there's a missing optimisation whenever you return a lambda or store it in a local variable that only gets passed to inline functions because the compiler doesn't realise that the lambda doesn't have to be boxed. If that optimisation is implemented, one could use a |
One question/clarification about value interfaces: how do value interfaces and non-value interfaces interact inheritance-wise? It doesn't make sense for a non-value interface to extend a value interface, but the opposite (i.e. Two questions about the scope functions section: If I understand your If I understand the var state: State = ...
state.tags += "tag"
state.updateNow() and var state: State = ...
state.apply{
tags += "tag"
updateNow()
} where the proper replacement of the first snippet is var state: State = ...
state = state.apply{
tags += "tag"
updateNow()
} This is different than how apply is used currently (the docs say "The return value is the object itself", and while the identity is irrelevant, this wouldn't do it with respect to equality either) and I expect it would trip people up. Especially in cases where you are using Given that we essentially want the lambda to match the call site's mutability, one easy way to handle this would be allowing overloads on Another thing that was hinted at a bit in the "Read-only collections vs immutable collections" section: |
@elizarov, first of all, thanks for taking the time to reply to our feedback. About these statements:
I agree when looking through an idealistic perspective, however, from a realistic perspective, I don't think it will be practical to have more warnings/boilerplate for mutability while switching the default to immutabilty considering Kotlin has to interoperate with Java and JavaScript, and these use mutability heavily. It's the same story as flexible types: ideally, everything would be nullable when dealing with platform types, however it would lead to an insane amount of null-handling code and noisy type declarations, so there's a pragmatic compromise of safety for interop conciseness. So this:
Has two potential issues: worse interoperability and more IDE dependency to know what's going on (which is bad for code reviews). The alternative is to drop the warnings and keep the idea of same syntax for mutable and immutable assignments, however we're then forced to look at the class declarations to know what's safe to mutate and what's not. The alternative @altavir and I proposed solves the ambiguity problem and I'm sure there are ways of designing something pretty concise so that the additional syntax shouldn't be an issue. Lastly, I'd like to reinforce what @altavir said about an easier mental model. Kotlin already has properties instead of getters/setters, which some complain "hide behaviors". It has delegated properties. It has dispatch and extension receivers. It's now going to have multiple receivers... |
@elizarov @edrd-f Let me reiterate since I've skipped some details that were present in the chat discussion. value class B(var c: Int, var d: Int)
value class A(var b: B)
var a = A(..)
a.b.c += 1 //returns Unit, but the state of a is changed
a.b.d -= 1 //second rewrite My thought (not originally mine, I am translating the result of the discussion) is to do a.mutate{ //or mutate(a){ which is probably even better
b.c += 1
b.d -= 1
} //both changes are done atomically here and the value As you see the syntax is exactly the same but is allowed only in a specific colored (see @ilmirus PR) scope. This syntax could actually co-exist with the first one to allow atomic changes and could be used for a restrict-first-relax-later introduction (only scoped changes at first, but also non-scoped later). The scoped change also has the benefit of always explicitly knowing, which variable is actually a root of a change. In a simple lens assignment, you can't know if the variable is the one that is being changed or an intermediate step. |
Did you mean to write |
Yes. It won't compile otherwise |
In the section Abstracting mutation into functions
It is not clear what is the return type of the
Yes, I agree.
Why need to cover with a language feature a deliberate, wrong programmer's choice? Finally it isn't not specified in that section how should work the code:
I don't find enough motivations for a new feature, moreover we should inspect Java interoperability better. final var state = State(...)
state.updateNow() // or StateKt.updateNow(state) This is a valid and misleading Java code. |
To help the developer to use immutable objects (see also my post above), we can introduce a new operator to "invoke and assign", really similar to "plus and assign" (with similar pro and cons). Here we play with a not mutable list.
The example in the previous post became:
This operator is more explicit because it has been read on the caller site, there is no magic under the hood, and it is more flexible because it is possible to use with already existent types.
or
|
Regarding the section Var this as a ref (inout) parameter in disguise, I try here to enhance delegation to achieve the same goal. Sincerely, I am not really a fan of this proposal, I don't perceive this feature as undoubtedly useful. My first idea is to use
My second idea is to allow delegation in function's arguments, i.e.:
Using these building blocks we can write code as:
What does it has to do with My first idea is to use
My second idea is to allow delegation in function's arguments, i.e.:
Using these building blocks we can write code as:
What does it have to do with
This implementation is a little more verbose (explicit) than the native one. |
This would be better discussed under https://youtrack.jetbrains.com/issue/KT-44585 (it is listed there as one of the possible syntactic options). |
Speaking of my app & library development experiences with Swift, the ambiguity as discussed is perhaps an unfortunate projection of the underlying implementation details of this language feature. Maybe it is caused by the close analogy to data class and its In my opinion, forcing mutations in a block scope indeed makes the intent to mutate stand out. But it is never going to tackle the "ambiguity" at its root, because the "ambiguity" is started before any mutation can happen — value classes effectively play the rules of value semantics. Conceptually, If we explain it in terms of value semantics, every rvalue is copy-assigned to the lvalue, notwithstanding the compiler potentially optimizing it away. This is why I find it slightly unfortunate that the document does not build on concepts like reference semantics v.s. value semantics, specifically for explaining changes in the language spec for the language users. Instead, it seems to be more geared towards implementors, with a wealth of details on implementation approaches. More specifically, while the document seems to indicate the initial implementation uses a copy-on-write approach, IMO it can still work well with value semantics + copy-assign being the conceptual model for value classes in the language. As far as I can understand, the procedural outcome should be the same — copy-on-write can be treated as an optimization to copy-assign, provided that there is no intervening optimisation. (like rewriting copy-updates into in-place mutations) I do understand that there might be a desire to set it apart from C/C++/Swift structs, but adopting these established and distinctive concepts as a tool to better contextualize the new feature need not imply optimization constraints. For example, the document argues that:
When we look at Swift, its language specification indeed does establish that:
While all these might seem to be agreeing with the presented counter argument, the Swift compiler does defy it — many compiler optimizations deviating from these semantics have been implemented and shipped anyways, as long as they did not change the procedural outcome or break the invariants. A couple of examples:
So... hmm, what I want to get to is, that this ambiguity feels more a teaching/explaining issue, rather than a trap warranting an in-language solution. I do think bringing in ref vs value semantics — as the new basis to explain normal classes vs value classes — could be helpful in this regard, rather than the current somewhat “a special kind of class” narrative. After all, AFAIK, no existing declarations except inline classes will be subsumed/auto-converted to value classes. Using a new type of declaration implies the need to learn new semantics. Sounds pretty fair to me. |
UPDATE: Taking into account use-cases where a value class is used as an immutable handle into a mutable data structure and the need to gradually teach developers new features where properties of immutable classes are somehow updated, we've decided to give an explicit name to the corresponding concept -- a "copyable property", and introduce a corresponding copy modifier both for "copyable properties" (copy var) and for copying functions (copy fun). The text was updated to consistently use the corresponding terminology and the copy modifier. Motivation for the change was added to the text, too. The restriction on the use of See #247 |
According to the last commit date, there was nothing changed near the date of your message |
I would not be able to offer insights of how advanced features are designed, but I can provide a very basic yet quite desperate use-case that we are currently facing BackgroundI am in the processes of writing some game server in my free time. In this server, we need to very frequently perform a wide variety of calculations on three dimensional vectors, ranging from thousands to around a million computations per second. Current Implementation MethodNaturally, we would want to pack it in a immutable class like this (computation functions omitted) data class Int3(val x: Int, val y: Int, val z: Int) But this gave me a very large performance compromise. JVMs are not able do proper escape analysis on this and very simple computations could result in expensive object allocations. So to make this perform better, I have to do the following: class MutableInt3(var x: Int, var y: Int, var z: Int) In this class, most computations do self mutations. In intensive computations, this gave me a 5x speed-up over the last version, but the performance is not ideal as this is still has an additional indirect access and it is now very error-prone. However, tens of thousands of such objects still fly around functions every game tick, so I have to further introduce a third way which is to manually writing each field wherever performance is needed, in the way of var x: Int = init
var y: Int = init
var z: Int = init This usually gave me a further 10x speed up in intensive computation regions. Note that val is not used here as it would otherwise be extremely cumbersome. Also, now I have to manually inline the computation functions to avoid object allocations for tuple returns, which made the code significantly bloated and caused problems on readability and maintainability as now there are a couple dozen of implementation of the same code dotted around the code base. ConclusionI desperately desire a simple syntax that does compile-time field-inlining for data-class-like structures. Even just simply spreading the fields into the parent scope or function signature whenever possible is better than the current situation of having absolutely nothing. I believe that this could encourage people to box their code in a reusable manner without worrying about absolutely tanked performance. Additional NotesI would prefer that fields for such grammar should stay immutable, as from my personal experience writing C# and C++, mutable value class structures actually cause confusions when parallelizing algorithms and working with async computation, as under such circumstances it is non-trivial to be sure that it any passed-in reference of a value object is safe to be used and mutated just based on the structure signature, and even if we are sure, we still need to rely on pure faith that no other programmers will break the current thread safety model. |
@CLOVIS-AI I agree that it would be less surprising to plainly forbid
@fvasco Actually that's a good point, I'm not sure how we could do that in a nice way.
@rnett This is indeed a good point, I guess mixing regular |
Overall I find the ideas introduced in the keep quite interesting and worthwhile (especially also the builder constructors). On "withers" (awful name - hard to teach unless it involves soul sand and skeleton heads), I think they are useful in working with immutable types (whether value or not).
In line with the comment above I think it is important to somehow define a syntax for semantically doing symbol reuse (if there is a reference floating about it now no longer refers to that object). While copy-on-write semantics are beneficial in cases, they can still be confusing and the code should allow consuming code to be understood without knowing that the types involved are immutable/use withers. The usage of A copy function (explicit writing) gets all kinds of issues with nesting and verbosity, I think an operator would be better. I would expect that in case of value classes, the compiler would optimize as appropriate (if it can elide the wrapper it should also be able to elide copies when setting member properties). |
It is very interesting to compare the behavior of already-existing "value classes" (eg. We are used to the fact that operators behave very differently on var a = 5
// This replaces 'a' by another value
// 'a' must be var
a++ compared to normal classes: val b = Foo()
// This calls 'inc()' on 'b'
// 'b' does not need to be var
b++ Another difference is how it behaves with other variables: var a1 = 5
var a2 = a1
a1++ // a2 IS NOT modified
var b1 = Foo()
var b2 = b1
b1++ // b2 IS modified, because of references In my experience, that difference is well-understood by everyone, and although it is a bit strange (unlike Java, it's not immediately clear what is a reference or isn't, especially for beginners), that difference is pretty much standard in all modern programming languages.
I'm currently working on a project with Kotlin React, where deeply immutable data structures are common (almost all the UI state), because React requires that the top-level reference be edited to detect a state modification. This leads to code looking like this: props.update(someData.copy(field = someData.field.copy(…))) Replacing these props.update(someData.field = …) I believe that, even if it uses the same syntax as mutable modification, and other variables/references are not updated automatically, this is much more readable and can be learned really easily. |
After re-reading the proposal, I am in favor of Using Although However, using If using the same keyword for both is important, I think this is fine; value class Foo() {
cov a = 5
cov fun foo() {
a = 6
}
} Here, From my experience working with ADTs as sealed class R {
val a: Int
}
data class A(override val a: Int) : R()
data class B(override val a: Int, …) : R()
data class C(pverride val a: Int, …) : R()
// This is necessary to be able to work with sealed hierarchies of immutable state, currently
fun R.copyA(newA: Int) = when (this) {
this is A -> copy(a = newA)
this is B -> copy(a = newA)
this is C -> copy(a = newA)
}
// Usage (example using Compose):
@Composable
fun Component(foo: R, update: (R) -> Unit) {
Text(foo.a)
Button("Update", onClick = { update(foo.copyA(10)) })
} This is necessary, because the Instead, something like this could be used: sealed value class R {
cov a: Int // or 'copy var'
}
value class A(override cov a: Int) : R()
value class B(override cov a: Int, …) : R()
value class C(override cov a: Int, …) : R()
// No need for any boilerplate copy function on the sealed class
// Usage
@Composable
fun Component(foo: R, update: (R) -> Unit) {
Text(foo.a)
Button("Update", onClick = { update(foo.a = 10) })
} Indeed, because sealed value class R {
cov a: Int
}
value class A() : R()
value class B(…) : R()
value class C(…) : R() |
@CLOVIS-AI While I agree we should consider
Yes it does. Same as for the Your argument does hold for
|
@joffrey-bion Ah, sorry. It seems I confused ++ (which assigns the return value, and doesn't mutate the object) with += (which mutates the object, without using the return value). But then I guess it's even more of a proof that this behavior (assign a copy, without mutating the original object, therefore without impacting other references) is not stranger to Kotlin, since that's how ++ behaves in all cases. If I've been able to read code without understanding the difference, I would not be surprised that reading code with value classes & deep copy wouldn't be an issue. |
@CLOVIS-AI I don't believe What does support your point is the inconsistency in the behaviour of This inconsistent behaviour is why I was initially suggesting using a distinct operator like |
in Java, that would just be a String, the actual ValueClass is not visible in Java.
data class Blah(
val value: ValueClass
)
Kotlin:
Blah(ValueClass("test"))
Java:
new Blah("test")
…On Nov 1 2022, at 8:33 pm, Cafer Mert Ceyhan ***@***.***> wrote:
Hey! I was trying to access a value class from Java but the code doesn't compile. Is this an interoperability issue or am I missing something?
—
Reply to this email directly, view it on GitHub (#237 (comment)), or unsubscribe (https://github.com/notifications/unsubscribe-auth/AARLC5QTEKJMQDRPI6XZ7G3WGFWANANCNFSM4W5CEPQQ).
You are receiving this because you are subscribed to this thread.
|
I was trying to use a value class as field, but it does not work:
I think it should create the class with an Int-field. I my project I access the classes with JNI, and it is easier to work with fields than methods in JNI |
I've got some questions about copying value classes, in comparison to data classes. I would be happy if someone could answer them and/or take into account. 1. Reducer-style functionsThere is a use case for copying data classes in a reducer-like style: fun reduce(state: State, msg: Msg): State Or in some cases I've seen the following pattern (something related to TEA architecture): fun reduce(state: State, msg: Msg): Pair<State, Cmd> The proposed solution with value classes is to use functions like fun reduce(state: State, msg: Msg): State =
when (msg) {
is Msg.Foo -> state.copy(a = 1, b = 2)
is Msg.Bar -> State(c = 3) // Reset the state with some properties changed
} For me it it would be pretty important to have this supported with copyable value classes. 2. Amount of allocationsI couldn't find this in the design notes, so asking here. When we copy a data class, there is only one allocation produced. var state = State()
state = state.copy(a = 1, b = 2, c = 3) With value classes this is going to look like this: state.a = 1
state.b = 2
state.c = 3 How many allocations will the second variant produce? This may be especially important when deep-copy the state. 3. State consistencyWhen we copy and re-assign a data class, the variable either points to the old object or a new one. The actual data is always consistent. Moreover, when we use If the state needs to be shared between threads, it can be marked as class Store {
@Volatile
var state = State()
private set
@MainThread
fun updateState() {
state = state.copy(a = 1, b = 2, c = 3)
}
} Or if the state can be updated concurrently, I can write the following: class Store {
private val _state = AtomicReference(State())
val state: State get() = _state
fun updateState() {
_state.update { it.copy(a = 1, b = 2, c = 3) }
}
}
4. Generic typesCurrently, one may define a generic Store for managing screen states (aka MVI/MVU), that accepts a reducer function for changing the state. class Store<State, Msg>(
private val initialState: State,
private val reducer: (State, Msg) -> State,
) {
// ...
} The Store is agnostic to the actual state type, it can be a data class, a normal class, enum, etc. It looks like that with the introduction of copyable value classes, the developer will have to choose: either use
|
Reducer-style function can be easily rewritten with the help of copying scope functions. The one you'll need here is copy-retrofitted version of
becomes:
However, as you'll use copy function throughout your frameworks, the whole approach to defining reducer-style functions will change. You'll declare them as copy functions on the state instead of using
Note, that this
The number of allocations will depend on the backend implementation details. WIth Valhalla-capable JVM or on a Naitive backend, for example, updates to value classes will not allocate at all. But even on pre-Valhalla JVM it will be possible in many cases to optimize
You can create a copy-friendly version of
Yes. You'll have to choose between the two signatures for your |
I'm a little concerned about current proposal on name-based construction, because it definitely can go too wild and messy when a class is mixed with properties in primary constructor, uninitialized properties in the body, calculated properties initialized in the class Game {
val id: Int
val name: String
var version: String
var vendor: Vendor
// ... more uninitialized properties ...
var repr: String
// ... more calculated properties ...
init {
repr = /* ... */
}
class Vendor(
val id: Int,
val name: String
// ... more (maybe even regular) parameters ...
) {
val repr: String
// ... more calculated properties ...
init {
repr = /* ... */
}
}
}
val game = Game {
// What do you expect?
} It seems too implicit and casual. I do think it's important to limit this feature for just data classes and value classes, because they have a stable source of where the core information are gathered, which is, the primary constructor. However, this restriction could seem tedious too. First, there are data-like classes which require regular parameters in the primary constructor. Second, it's quite common to have more stuff than the default behavior, like to add parameter checks and transformations. By the way, it seems that a builder object is required with current proposal. For normal classes, this is fine. What about value classes, a more lightweight class model? Is it appropriate to have an object anyway? Probably not. We need a more performant means to achieve this, at least for value classes, especially when it is totally doable to flatten multi-field value classes into local variables or function parameters. Please, do not count on JIT all the time. All in all, we need to design this feature with three principles in mind:
I'd like to share a design for this entire system.
|
Would it be a good idea to make the value class itself implement Comparable if the wrapped value implements it? E.g. this code currently does not work:
The following code works as a workaround:
Many developers assume that the value class automatically adopts such traits of the wrapped value and basically delegates implementations to the wrapped value. But |
@Davio what if I wanted my value class to sort in a different way than the inner values? Example: @JvmInlint
value class Version(val version: String): Comparable<Version> {
override fun compareTo(other: Version): Int = // compare based on semantic versioning
} Unsure with your proposal if I would feel safe making value classes. I might always be worrying if I rememebered to write my custom |
@mgroth0 you can already override the default working of |
I disagree. I create value classes when I want to create new values with new behavior. If I wanted to keep the same behavior, I would use a |
@CLOVIS-AI you could also create value classes because you want to enforce some requirements, such as a value class which wraps an Int that must be in some range, or a value class which wraps a String that must have a certain format, etc. You can't do this with a typealias, so there are use cases for value classes that closely want to simulate their wrapped values, but offer something extra. |
I agree that this is a valid use-case, for example, to represent semver tags, date representations, vehicle license plates, etc. However, in all these situations, comparing by textual representation is meaningless. In my opinion, there shouldn't be a default behavior if it's likely to be incorrect. However, I do agree that the syntax for interface implementation is too verbose in this situation, especially when interface delegation is available to other classes. I created KT-67167 to track this. |
Good point, but this is exactly why I avoid |
@Davio, why the Comparable interface only?
I consider this a wrong assumption. |
@fvasco Not the Comparable interface only, but that is one that I had a use case for. I'm not sure whether the change would not be backwards compatible. Value classes that already explicitly implement But I guess it all depends on what your view is of value types:
|
now
It wouldn't be. If you mix old libraries with new ones, you will fail.
then you probably need Nevertheless, I agree that we should provide some [explicit] way to provide |
Hi @Davio,
So, in your example, If I understand your idea well, you want promote my value class Password(private val secret: String) {
override fun toString() = "****"
} to value class Password(private val secret: String): Serializable, Comparable<Password>, CharSequence {
override fun toString() = "****"
// all other auto generated methods
} implicitly and without any notice. This proposal isn't back compatible and introduces bugs on my code. For these considerations, I think that this proposal does not comply to the Principle of least astonishment and the Minus 100 points rule. I agree with @zhelenskiy, maybe you are looking to |
@fvasco I guess that you can find use cases for both arguments, those were it would be a good fit and those were it would not be. So the question is: would you expect (or want) a value class to have all the characteristics of the type it wraps (and I concede that Comparable is a weird one because the T should always be the type of the class itself, so I'm not completely convinced either way, I think there are arguments for both, but whatever is decided is fine. We can implement my use case perfectly by writing explicit implementations.
But writing a compiler plugin could be a nice middle ground I guess. |
@Davio, I cannot reply to your question becouse a "Values classes are immutable classes that disavow the concept of identity for their instances". From the referenced KEEP @JvmInline
value class Complex(val re: Double, val im: Double) Is Same considerations are valid for a single-field value class: @JvmInline
value class Optional<T : Any>(private val value: T?) {
fun isEmpty() = value == null
fun get() = checkNotNull(value)
}
I sincerely suggest you to re-read the referenced KEEP deeply. |
This issue is for discussing and gathering feedback for Design Notes on Kotlin Value Classes: https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md
The text was updated successfully, but these errors were encountered: