Hi all, just a general design choice question abou...
# getting-started
t
Hi all, just a general design choice question about value classes. If I create a value class, I am only allowed to have exactly 1 constructor parameter. If I then use this type, I still have to treat it as a class with a variable. If the compiler knows Foo only has 1 variable, and it knows its type. why does the compiler make me use myFoo.value instead of just myFoo?
Copy code
@JvmInline
value class Foo(val value : Int) {
}

//This doesnt work
fun bar(myFoo : Foo) : Int{
   return myFoo + 1
}

//This works
fun bar(myFoo : Foo) : Int{
    return myFoo.value + 1
}
r
Because the point of a value class is to make a lightweight new type, not a type alias.
👍 1
c
The purpose of value classes is to create a new type, generally to avoid confusion (passing around Ints or Strings can be fragile); as such, the compiler sees two types:
Foo
, for your value class, and
Int
, for Foo.value.
2
t
Well, the thing is.. I kinda would like it to be a type checked class, with the convenience of an alias
Maybe i should have been more clear there, type classes are good for intent and usage, but it doesnt do type checking, value classes do type checking, but you need to do constructor calls + parameter usage, i would like constructor call + usage as alias
i have no problem doing Foo(7) instead of just passing 7, but why do i need to do myFoo.value instead of just using myFoo (since the compile knows myFoo.value is the only possible member, and knows its an Int)
c
there’s no direct way to convince the compiler that
Foo
(value class) is an
Int
(value type), as there are two types in play. You can dereference
.value
and/or provide operators or other convenience/extension methods to make usage of
Foo
more idiomatic for its value type.
t
I know i can do custom operators for this 🙂 I was just wondering, isnt it basically polymorphism, as in a Foo is also an Int (since the value class Foo can be identified by its value, which is an Int) (im just basically thinking out loud/ theory crafting)
c
that’s the point of value types - to distinguish based on type. Otherwise, a method like
something(a: Int, b: Int)
could be called with
Foo
transparently, which puts you back at type aliases.
t
not really
c
how so?
t
since somthing(a : int, b : Foo) could then be called with something(1, Foo(5))
but in somethings body, Foo(5) could be used as int
So type checking on input, but after the typechecking has been done, treat it as an alias
(unless something calls another function which needs a Foo, then you could put in b, but not a)
c
partial relaxation of types safety sounds messy and frought with problems.
t
basically
Copy code
@JvmInline
value class Foo(val value : Int) {
}


fun bar(a : Int, b : Foo) =  a + b

bar(1, Foo(2)) // would work
bar(1, 2) // would not work
bar(Foo(1), Foo(2)) // would work
There should not be a type safety issue here
Since at runtime this is basically exactly what happens with value classes
Foo(1) can be replaced at runtime with 1
so basically Foo is an Int, but an Int is not a Foo
(Which makes it different from aliases where Foo is an Int and Int is a Foo
r
Unless I am mistaken, a value class's parameter and/or constrictor can be private (to effectively hide the underlying type in the API) which would not play well with what you propose.
t
That bit would still apply right? This is basically only replacing myFoo.value with myFoo. So the only thing it would really effect is where you are interested in myFoo instead of the value inside of it
No difference on construction, basically only syntactic sugar that the compiler changes myFoo to myFoo.value for me
r
That would still reveal the underlying type in the API, which is exactly what private doesn't mean.
If
value
is private, the user cannot use it directly, which is effectively what you are proposing.
t
Well, if it's private, I would not write myFoo.value anyway
r
I would also say it doesn't make sense semantically. If I have a value class of exit code with an underlying
Int
for the sake of POSIX compliance, allowing
fileNotFound + 1
is nonsense.
That's why you are allowed to add the functionality of you want, but sticking it on by default would be bad.
t
Well, if I want to do + 1.. it doesn't stop me from doing ExitCode.value +1
If you would not want to use the underlying value, you can make it private
In which case you protect yourself from ExitCode + 1
But if I have a Positive(2) in practice I would want to use the underlying value all the time
Basically, if the constructor is not private.. you are mostly interested in the underlying value for doing actual work
And if the default behaviour is the thing that is most important, just to continue the thought experiment let's assume my proposed behaviour only applies when you declare it as value class Foo(open value : int)
n
The whole idea behind value classes is to avoid mistakes like adding a temperature value to a pressure value. But you could use
Copy code
@JvmInline
value class Foo(val value : Int) {
    operator fun plus(i: Int): Int {
        return value + i
    }
}
t
I'm aware it's possible.. but after a good other discussion what I'm basically looking for is a restricted type (without the disadvantages of a wrapper). As in, e.g. I want to have an int that only contains even numbers. It's more of a thought experiment than how to model it in kotlin, and Kotlin would get really close to it with the suggestion I made.
And also thanks to everyone who replied:) the insight I got is the phrasing: restricted type
n
and a
value class Even(value: Int)
with appropriately overloaded operators would not accomplish that? Of course, you would still not get a compile error for
Even(3)
unless you use something like
Copy code
@JvmInline
value class Even private constructor(val value : Int) {
    companion object {
        fun ofTwice(i: Int) = Even(i * 2)
    }
}
and your "polymorph types" argument does not fly well with Kotlin's "no implicit conversions" approach
t
I am fully aware I can operator overload or just call .value on it. But the whole point was more that it seems a bit boilerplateish. Basically it's indeed a implicit type conversion (basically autoboxing) that should be fully type safe.
Copy code
@JvmInline
value class Foo(val value : Int) {}

fun bar(a : Int, b : Foo) =  ???
bar(1, Foo(2)) // would work
bar(1, 2) // This will not work
bar(Foo(1).value, Foo(2)) // would work
bar(Foo(1), Foo(2)) // and this doesnt (and this was what my thought experiment was about)
n
so you are arguing that
1 + Apple(2) + Orange(3)
should result in
6
?
t
if Apple and Orange are both value types which are fully identified by their Int mapper, then yes
int attribute*
because its basically 1 + Apple(2).value + Orange(3).value
at runtime it will translate to 1 + 2 + 3, since the wrapper classes will be removed at runtime
(thats the whole idea behind value classes)
ofcourse Apple("a") + Orange(2) wont compile
And fun aaa(a : Apple) aaa(Orange(2)) also wont compile
n
not in my book: I would never add apples and oranges (or meters and feet) w/o explicit control. The whole point I see in value classes is to get the runtime efficiency w/o the type safety issues. Else, why not just use type aliases?
t
But you can still do Meter(1).value + Feet(1).value, and that will just compile
Copy code
fun doSomething(m : Meter) = ???
will still not accept doSomething(Feet(1))
Copy code
fun doSomething(m : Meter, f:  Feet) : Int = m+ f
this will be possible though
but the fact that you use a + here means you want to operate on the values of it
So these conversions are still fully type safe
n
I think we have to agree to disagree here, because I really do not see any point in your
m + f
.
t
the point is more that fun divide(a : int, b : NotZero) = a /b is quite nice
n
so could be
"hello" + 3 == "hello3"
, but I still think the gain does not justify the potential pain (esp. given that I can always implement this if I really want to). Anyway, OO for me now
t
Kotlin actually supports that
fun doSomething(m : String, f:  Int ) : String = m + f
`
y
I would like to add that I think @Ties’s use case is legitimate. For instance, a
value class Positive
could arguably be useful as its underlying int since it only refines the type. Maybe this should be a new language feature? In fact, I suggested a while back the idea of
transparent value class
which would be considered both its own type but also its underlying type: https://kotlinlang.slack.com/archives/CQ3GFJTU1/p1629640887047500 https://kotlinlang.slack.com/archives/CQ3GFJTU1/p1629640887047600
♥️ 2
k
Sometimes other languages can be useful for bringing ideas to a new language. Ada has a strong type system in which you can refine the primitive types, so you could indeed have a type called Positive, for example. This snippet gives a flavour of what it's like:
Copy code
procedure Main is
   type Distance is new Float;
   type Area is new Float;

   D1 : Distance := 2.0;
   D2 : Distance := 3.0;
   A  : Area;
begin
   D1 := D1 + D2;        -- OK
   D1 := D1 + A;         -- NOT OK: incompatible types for "+" operator
   A  := D1 * D2;        -- NOT OK: incompatible types for ":=" assignment
   A  := Area (D1 * D2); -- OK
end Main;
❤️ 2
r
Correct me if I'm wrong, but this seems to be the exact opposite of what @Ties is asking for. This shows a sub type that cannot be mixed, whereas they're asking for a new type that can be mixed.
t
Well, my use case was actually not mentioned in the last post, distance and area are 2 separate types, but I suggested that a function that can have a float as input could also have a distance as input
I do wonder if Float a = D1 + A would work (that's basically part of what I suggested, as in, it cannot be a D like in the example, but it could be a float)
(I think so, since you can create an Area with a D as input), that would only work if D1 * D2 is both a D and a Float
y
I think that basically with something like a
transparent value class
(having a keyword like transparent makes it very explicit which is good) maybe we can consider that value class to just be a subtype of its underlying value. And so, in a way, Foo would be a subtype of Int, and so, just like a normal subclassing relationship, if
Foo + Int
is not defined,
Int + Int
will be used instead.
t
Indeed :) so Foo + Int won't produce a Foo, but will produce an Int (since that's the closest common type)
y
And obviously you then could, if wanted, to define a
Foo + Int
that returns a
Foo
👍 1