Is there any way at all in Kotlin to be able to ha...
# announcements
n
Is there any way at all in Kotlin to be able to have a new constraint on generics, that is met by existing types? AFAICS, generics are only constrainable via inheritance, and you can't make a type you don't control inherit from one of your classes. Is that all there is to it? Is a solution for this being discussed anywhere?
t
Kotlin doesn't have union types and they are the only way I could spontaneously think of for doing something like this directly without a workaround. Depending on your use case you might be able to define a wrapper class that implements your own interface and delegates calls to a different object. However, before doing this I suggest questioning whether you really want or need a generic parameter that mixes your own types with external ones that don't implement a common interface.
n
well, there are two ways, right
the other way would be if you could have a notion of implement an interface for a class, outside the class
e.g. in Rust, you have traits, you dont' need to own the class to implement the trait, just either the class or the trait.
If in Kotlin the owner of an interface could implement that interface for other types, then that + sealed classes would work
a third way is to have unconstrained generics, or at least opt in, but that's not a popular approach (just C++ I guess?)
t
ok, yes, that would also work if there was something like scala's implicit classes.
n
And the use case is pretty straightforward, it's just having a nice json API
you want an in-memory data structure that mirrors a json document
t
unconstrained generics are possible in kotlin, but obviously it comes at the cost of type safety
n
the ideal is to have basically JsonValue = Union[NullType, String, Int, Float, List[Jsonvalue], Map[String, Jsonvalue]]
well, it's not obvious 🙂 In C++ unconstrained generics are still type safe
t
how can it be typesafe if you don't know the type? sorry, I don't know much about c++
n
well, it's type safe at the point of use
the user of the generic causes the generic to instantiate with some specific type
and then, that can fail compilation
basically in most languages, the generic compiles separately, then the user need only pass in a type that meets the generic constraint, and then from that point on, compilation errors are impossible.
in C++, the generics don't have constraints, but if the user passes in an incompatible type, the user gets a compiler error
at any rate, json obviously isn't the most common real world case, i picked it to try to exercise the type system a bit, see what kotlin looks like
t
ok, that makes sense. But regarding your actual question: Kotlin doesn't have union types.
n
yeah. I don't mind sealed classes, boilerplate wise.
It's not the end of the world.
However, the fact that sealed classes are intrusive is a much bigger problem.
Hence why I'd also be open to some kind non-intrusively allowable constraint
imagine it's called a protocol. It's like an interface, except that the owner of the protocol can implement it for types they don't control.
now, I can have a trivial protocol, i.e., it doesn't need any methods
then just implement the protocol for String, Int, List[JsonValue], etc
now I can constrain things in a sensible way
f
You cannot implement arbitrary traits for arbitrary structs in Rust. It's limited to traits and/or structs in your module. You can use an inline class that implements the interface and delegates to the external class.
Or an object, overloads come to mind too. 😃
n
@Fleshgrinder You need to own the trait or the struct
in this case, that's fine, since I would be defining and owning a new trait (and the trait would even be an implementation detail)
The difference in Rust is that you only need to own one or the other. In Kotlin, you always have to own the struct (class).
The suggestion of using an inline class doesn't seem to work
you can only delegate to interfaces
Int also seems to be final on top of that... And I'm getting more errors on top of that
Copy code
inline class foo(val x: Int) : Int by x
gives me a pile of errors basically
f
Delegation doesn't work with inline classes. However, an
object
works.
n
can you clarify?
note that
class foo(val x: Int) : Int by x
does not work either
f
Int
isn't an interface, hence, it cannot work.
Number
would. Gimme a sec, I'll prepare an example. In Rust terms: you are trying to
impl i32 for foo
which isn't possible there either.
Untitled
This works, always, even if
C
is from a different library.
n
Number isn't an interface either
Also, no, this is not what I'm trying to do
f
Then I misunderstood, can you elaborate (or show me the code)?
n
I was writing some code that is the in-memory representation of json
f
True,
Number
is an abstract class but that's close enough I'd say (since you CAN extend it).
n
Kotlin still will not let you delegate to it
So, to store json, I did something like this:
Copy code
sealed class JsonData {
        class IntWrapper(val x: Int) : JsonData()
        class StringWrapper(val x: String) : JsonData()
        class ListWrapper(val x: JsonArray) : JsonData()
        class ObjectWrapper(val x: JsonObject) : JsonData()
        class NullWrapper() : JsonData()
    }
f
That's because your delegate must implement the type, otherwise Kotlin doesn't know how to delegate. Hence, delegating from an
Int
to
Number
works but not
String
to
Number
.
n
Then I have a class
JsonValue
with a member
var jsonData: JsonData
Copy code
class glug(val x: Int) : Number by x
this does not compile
Even though Int implements Number.
"only interfaces can be delegated to" 🤷
anyway
f
True 😄 dealing with too many languages to remember all caveats. 😛
n
My JsonData sealed class is obviously trying to replicate sum types in typical kotlin fashion
f
Continue
n
this works well enough for internal representation
the problem is when it comes time to write a function that makes JsonValues
Copy code
companion object {
        fun make(x: Int): JsonValue {
            val j = JsonValue()
            j.jsonData = JsonData.IntWrapper(x)
            return j
        }
        fun make(x: String): JsonValue {
            val j = JsonValue()
            j.jsonData = JsonData.StringWrapper(x)
            return j
        }
        inline fun <reified T> make(x: List<T>): JsonValue {
            val j = JsonValue()
            j.jsonData = JsonData.ListWrapper(x.map { JsonValue.make(it) })
            return j
        }
    }
that's the beginnings of my attempts
the problem here is that basically I want my
make
function to work for
List<Int>
,
List<String>
, and so on
not surprisingly for json, my make function is recursive
f
Yeah, here you need to use overloads.
n
(I had to get rid of the inline and reified because of this)
meh, you can use overloads to solve the problem one layer deep
but you can't solve the problem properly with more overloads
if your
make
function doesn't call itself recursively then it's not going to work in the ideal way
The problem here basically boils down to this: 1. Kotlin generics can only be constrained by base classes (or interfaces) 2. There's no way to make an existing class have a new base.
As a result, if i want
make
to properly work, including recursively, for built in types
I'm boned
Here's what I ended up with instead for
make
oops sorry
Copy code
fun make(data: Any?): JsonValue {
            val j = JsonValue()
            if (data is Int) {
                j.jsonData = JsonData.IntWrapper(data)
                return j
            }
            if (data is String) {
                j.jsonData = JsonData.StringWrapper(data)
                return j
            }
            if (data is List<*>) {
                j.jsonData = JsonData.ListWrapper(data.map {make(it)}.toMutableList())
                return j
            }
            throw Exception("oops$data")
        }
This solution works, and it is recursive
So for example, I can do this:
Copy code
val list = listOf(1,2,3)
    val j2 = JsonValue.make(listOf(list, list, list))
f
That's basically exactly what the compiler would generate too. Now add overloads to the mix (for your public API) and it's type safe.
There's no nice way to solve this, sadly.
n
Well, it won't be properly type safe, like I said
even with boilerplate, there just is no way to solve this in the Kotlin type system
You can make overloads that are type safe, that are one level deep
you can have overloads that take List<Int>, or List<String>
but you can't keep going... what about List<List<Int>>, and List<List<String>>, etc
in order for this to be solvable you need one of two things (and some other possible solutions with more of a workaround flavor): 1. Proper union types. 2. A way to add something like an interface/base to a class you don't own.
f
The public API of this is type safe.
n
How is this type safe?
what happens if I pass
listOf(ArbitraryClass())
f
You are not exposing anything to the public that takes
Any?
, only the types you actually support.
n
? You are exposing
List<Any?>
What you have here is "one level deep" type safety
It's not actually typesafe
f
List<Any?>
is good enough for JSON because JSON arrays contain arbitrary data.
n
... no, they don't contain arbitrary data
Each entry is itself a json
f
Which may be of
Any?
type. 😉
n
which can only be null, string, numeric, boolean, another list, or a map
Yes, they are of Any? type, but Any? type includes things that are not valid json
f
listOf(ArbitraryClass())
is covered by
List<Any?>
What
Any?
type are you thinking of?
n
? ?I think you can see that
that should be a compiler error?
that's the whole point of being "type safe"
It should accept
List<Int>
,
List<String>
,
List<List<String>>
, etc
f
I think we've established early on (before I joined to try and help) that Kotlin doesn't have the machinery to solve this with the help of the compiler.
n
but not
List<SomeClass>
well, yeah, that's more or less where I was at, but you are claiming this thing is type safe when it's not
f
You can define a recursive dynamic type.
n
That's more or less what I did, but it's not type safe in that case
But note that the only reason why this can't be both type safe, and work properly, recursively.
Is because you want to work in terms of standard library types, like Int, String, etc, conveniently
So, it's not like Kotlin's type system is way off here. It only needs one of the two features I said. Basically, a way to specify sum types non intrusively, whether that's through sealed class + non-intrusive inheritance
or through proper union types
f
All these features are actively discussed. 🙂
n
That would be cool 🙂 Do you have a link?
looking at kotlin I feel like sum types are really the only feature that stands out, as expecting a modern language to have, but lacking
I thought sealed classes would be good enough, but walking through this example made me realize how big a deal intrusiveness was. I thought I could work around it with wrappers, but you can't really.
f
https://github.com/Kotlin/KEEP/pull/87 this comes to mind but you'll find endless discussion here in the Slack channels and in the Kotlin forum around these topics.
n
which slack channel are these things usually discussed in?
f
I once required a dynamic type to represent JSON values. I don't know if this is even remotely useful to you in your case but maybe this provides some help. If not, well, ...
#C0B9K7EP2 is the correct channel for these things.
n
whoa that's pretty long 🙂
I didn't need to resort to a dynamic type
Like I said, that
JsonData
sealed class works pretty well
and it's much better than being fully dynamic
the only problem is in writing a recursive, statically typed factory function
Like, e.g. this signature:
Copy code
fun maybeGet(index: Int): Dynamic? =
I don't agree with
It's certainly possible to do better than this in Kotlin
the appropriate return type is the same as the parent class 🙂
f
This class is to deal with JSON documents without knowing the types or structure of the document. You decide at the call side what you think it is.
n
Copy code
class JsonValue {
...
    fun maybeGet(index: Int): JsonValue?
}
f
That's why I said: might not be useful to you at all.
n
yep, that is true about my class as well
my class doesn't constrain the types or the structure
just restricts it to be valid Json, that's all
f
This doesn't either.
n
Yes, exactly, your type doesn't restrict at all 🙂 It's fully dynamic.
f
As I wrote earlier,
Any?
is valid JSON.
n
As I wrote earlier, Any? is a superset of valid json 🙂
it's a recursive format, right? So, more or less by definition, whatever class you define as holding json data
maybeGet
should return that class (nullable)
oh wait, your class is called Dynamic?
haha
my bad
f
You confused me here for a while. 😄
n
Yeah i was confused, I was thinking of the
dynamic
keyword
f
It returns itself and now you decide what it should be.
n
Yeah, that makes sense, though it's not a great name IMHO
f
someJsonList.maybeGet(0)?.unwrap<String>()
n
yeah, i have pretty similar syntax.
there's only a handful of legal types it can be though, of course
f
getOrNull
would be more idiomatic Kotlin, this class is a little older than that convention.
n
(well, technically infinite because it's recursive but you know what I mean)
Yeah
the
.unwrap
is another great example though
you can probably put any arbitrary thing there
and only get a runtime error
f
The naming is Rust-ish and not Kotlin-ish 😄
n
if you do
.unwrap<SomeUserType>()
that should really be a compilation error
f
Of course it's a runtime error, it's always a runtime error, because the JSON you deal with is a runtime value.
n
that doesn't mean it has to be a runtime error...
asking a sum type if it's something outside of one of its type arguments, shouldn't be a runtime error, right?
It should be a compile time error
f
How should the compiler verify that you can unwrap
SomeUserType
without knowing about your JSON document and actually understanding if that JSON value can be deserialized to your
SomeUserType
?!?
Especially if the JSON is received via an HTTP call at runtime?
n
Err, the same way that the compiler can look at
x: Union[Int, String]
and reject something like
x.unwrap<List<Int>>()
}
?
f
You defined that
x
can only be of
Int
or
String
but that doesn't say anything about a random JSON value you retrieved at runtime.
n
If you have a Union/Sum type, you know you have exactly one of the types in the Union/Sum. Asking for any other type, is a compilation error
A random json you retrieve at runtime can only be None, Int/Double/Number, Bool, List[Json], Map[string,Json]
that's it
json is a recursive sum type
f
JSON is
Any?
because we don't know if
{"foo":"bar"}
can be deserialized to
SomeUserType
and/or
SomeOtherUserType
at compile time.
n
deserializing is separate
I'm only talking about unwrapping here
f
Gotcha, you're right. However, could be solved once more with overloading
unwrap
. 🙂
Actually, no, because of type erasure. But with separate
asString
,
asInt
,
as...
functions.
The
List
and
Map
cases are covered with the
asList
and
asMap
functions.
n
it can't be handled, once you start dealing with the recursive nature of it
basically, Kotlin's pretty inelegant because you need wrappers for everything to use sealed classes for sum types
f
asList
returns
List<Dynamic>
and you need to decide once more what it is.
n
however, for a non recursive sum type you could brute force it with overloads
Sure, so you get around the problem by forcing the user to specify the types one by one 🙂
but in principle, if the user knew they wanted
List<List<int>>
immediately
theys hould just be able to cast to do that
*cast to that
f
Well, that was the goal for my
Dynamic
class.
n
Yeah. I mean these are the limits of what can happen in Kotlin.
In C++ for example though, you can do that
you can do things like
Copy code
auto j = json::parse(....)
vector<vector<double>> = j;
this will work
but
Copy code
struct foo{};
vector<vector<foo>> = j;
will give you a compile time error
f
The above is possible with JSON libs like Jackson too.
n
Based on what we've talked about here, unless the java type system has something on the kotlin type system
I don't see how it's possible
At most, you can hardcode down to a finite depth, so I suppose they could have done that.
f
Copy code
auto j = json::parse(....)
vector<vector<double>> = j;
This can never give you a compile time error because the compiler doesn't know what
...
is. If it's
"{}"
it's not a
vector<vector<double>>
. What the compiler may know is that both
vector
and
double
are possible values for the return (incl. nesting) but not if the parse result is actually a
vector<vector<double>>
.
In the end it's all about the possibility to define something like
typealias JsonValue = Boolean | List<JsonValue> | Map<String, JsonValue> | Number | String
(although
Long
is actually not entirely representable in JSON but that's another story).
n
Yes, I agree with the second thing you said
with the first, you misunderstood what I said
vector<vector<Foo>> = j;
is a compile time error
f
Always possible 😉
n
Well, as we established, it's not really possible in Kotlin
and I suspect not in Java either
the only way to get this sort of compile time safety in kotlin/java is via overloading, and that can't be recursed indefinitely
ultimately if you want it to work you either need to have unconstrained generics (like C++), or way to express the constraint properly. TO express the contstraint properly, you need a sum type over classes you don't control, which Kotlin/Java cannot do.
f
Exactly, yes.
m
I think Arrow Meta has a compiler extension to allow union types, but it would be better to ask in #CJ699L62W to be sure. Also this library emulates union types in standard Kotlin: https://github.com/renatoathaydes/kunion