can anyone help me understand what's going on with...
# random
t
can anyone help me understand what's going on with a
value class
not working as expected. it seems like i'm doing stuff the way the docs say should work, but it's not actually working. (details in thread)
specifically, the delegation described here isn't working https://kotlinlang.org/docs/inline-classes.html#inline-classes-and-delegation
here's my minimal example
the error from the intellij scratch file is
Copy code
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch
my understanding of inline value classes was that they were unwrapped at runtime and automatically had all the methods and properties of the wrapped type. is that wrong?
c
Look at the example from the docs again. The class implements MyInterface by delegation. Your class does not
t
i see. so that only works for interfaces - is there a similar pattern for auto-unwrapping the value to access it?
i mean, i can of course make the parameter not private so it can be accessed, but that seems to kind of defeat the point of an inline class that should just kind of fade into the background except for type enforcement
c
The parameter should be public. It is fine to access it
t
ok
s
In my opinion, the main reason for introducing value classes is for type enforcement.
t
yeah, the
inline
aspect kind of makes it feel like it should just auto-become whatever it's wrapping under the hood, but i get why it doesn't - both from technical and philosophical perspectives
a
They’re unwrapped at compile time where possible or “boxed “ like Java boxing works when they’re needed as a reference (generics is one) . There’s no automatic “here is the methods automatically “aspect of it, you can add methods to the type or just expose the val publicly and call uppercase on its property
r
Perhaps you just want a
typealias
?
s
The
typealias
will essentially be that, but that would remove the type safety aspect from it:
Copy code
typealias UserId = String
typealias ItemId = String

fun getUser(id: UserId) {}
fun getItem(id: ItemId) {}

fun main() {
    val itemId: ItemId = "foo"

    getUser(itemId) // this would still compile
}
Copy code
@JvmInline value class UserId(val value: String)
@JvmInline value class ItemId(val value: String)

fun getUser(id: UserId) {}
fun getItem(id: ItemId) {}

fun main() {
    val itemId: ItemId = "foo"

    getUser(itemId) // this would not compile
}
t
type alias doesn't do the type enforcement an inline class does. i think we are going to go the "add members to the inline class" path
👍 1
r
type alias doesn't do the type enforcement
But, isn't that exactly what you're asking for? How do you expect both "I want the new type to be treated like the wrapped type" and "I want the type enforcement for the new type"?
t
the type enforcement allows you to avoid putting things in the wrong place. just because it's all strings under the hood doesn't mean that the data in the strings is interchangeable.
but just because i want to force logical separation in the type system to make sure it's clear that values aren't interchangable doesn't mean i don't want/need to manipulate the underlying value
r
Right, and there's nothing stopping you from manipulating the underlying value. My question was about treating the new type as the underlying type. Perhaps I'm missing something, but the way I'm understanding the question, you seem to want it to act like both a new type and a type alias, and when it chooses which behavior is a rather arbitrary "when I want it to", which doesn't seem to be meaningful in practice.
At least, not until AI gets a whole lot better...
t
i'm not looking for it to auto-convert the underlying type to the inline type or vice-versa, but it would be nice that members of the underlying type be available without the underlying value having to be public
but yeah, it's not how it works, it just wasn't super clear from the docs because the docs always show the member as private and then there's the delegation section showing that you can treat it like the other type but the examples aren't clear that it's only valid on interfaces
r
But again, that's pretty arbitrary, and only applies when the new type is effectively a wrapper of the underlying type. For example, it wouldn't make much sense (to me) to allow things like
.substring
on a
value class Regex(val pattern: String)
, or
red / green
on
value class Color(val data: Int)
, so it still feels like a pretty arbitrary "when I want it to" to me, if that makes sense.
In the docs, it's not that it's "only valid on interfaces", it's that the new type implements those interfaces. That's no different from any class implementing an interface. Even a normal class can delegate an interface implementation to a constructor parameter.
Copy code
class MyInterfaceWrapper(
    val myInterface: MyInterface
) : MyInterface by myInterface
That's still perfectly valid, no value class needed.
t
yeah, i know, but the examples in the docs kind of make it look like you can just use it like the underlying type. i know because it's what someone else on my team thought and asked me about it and then when i glanced at the docs, it looked like that to me. i understand how it actually works now and the docs do say in the text how it works, but that doesn't mean that someone who glances at the examples in the docs will immediately grok the details
and when i say "only valid for interfaces" i know it's about the implementation but you can't do implementation by delegation for anything other than interfaces, which is what i meant
r
Ah. my bad. That makes sense.
I guess I see where you're coming from with the docs potentially being confusing, but considering it's under a big headline that states "Inline classes and delegation" at the bottom of the page (even after a discussion of the differences between value types and typealiases), I'm not sure what more they can do to show this is specific use case and not the general case. If you have an idea, I'm sure they'd love suggestions.