CLOVIS
02/03/2025, 7:03 PMinterface Value<T>
infix fun <T> Value<T>.eq(other: Value<T>): Value<Boolean>
So far, this is quite simple and well-solved by Kotlin. However, there are types which can be seen as Value<T>
which don't implement the interface, because I don't own them. In this particular case, there are 4:
• instances of the interface Value<T>
, for example as returned by other operators
• instances of T
(the regular Kotlin type)
• instances of KProperty1<*, T>
• instances of Field<T>
(another type, describing what it is is not relevant to this discussion)
Solution 1. Overloads
Therefore, to typesafely accept all of these values, I would have to declare all operators 8 times (since they accept two operands which can each have 4 different types). Since there are more than 30–40 operators, that would result in a lot of declared extension functions, which is probably not great for compilation performance.
someValue eq true
Solution 2. Conversion method
An alternative solution that is currently possible is to have the user call a conversion method to convert non-Value<T>
types. For example, it could be called of
.
someValue eq of(true)
This is, by far, the simplest solution at the moment. However, it implies a burden on the user, and calling of
everywhere can make usage quite verbose:
of(User::score) set cond { of(User::role) eq of(User::candidate) }
.then { of(1) }
.else { of(User::score) add of(1) }
I think you'll agree that this is quite a bit harder to read than
User::score set cond { User::role eq User::candidate }
.then { 1 }
.else { User::score add 1 }
Because it burdens the user, I don't think this solution is what library authors should use at the moment. However, it can be a part of a larger solution. For example, having these conversion functions makes the overload solution trivial to implement, as all overloads just delegate to the main one.
Proposed solution 3. Proper union types
Based on the way the problem is described, the operand types could be declared as
union Operand<T> = Value<T> | T | KProperty1<*, T> | Field<T>
There already exists a lot of discussion on union types, so I won't detail this solution further.
Proposed solution 4. Fake union types
This idea is similar to union types for callers, but it doesn't require union types to exist at runtime. The idea is to coalesce all cases into a single regular type through a conversion function, and Kotlin compiler just has to call the provided conversion functions. In this example, the canonical type is Value<T>
, so we could imagine a new operator that converts from other type.
interface Value<T> {
// …
operator fun from(other: T) = …
operator fun from(other: KProperty1<*, T>) = …
operator fun from(other: Field<T>) = …
}
In a way, this is very similar to C++'s implicit conversion methods, but declared in reverse: instead of a type describing what it can be converted into, a type described what it can be converted from. I believe this is much safer and less surprising, but it may still be much too surprising. This could also be very badly abused if the conversion methods aren't pure (in an FP sense).
Proposed solution 5. Typeclasses
I know that other languages have used typeclasses to solve this issue. To be honest, I'm not fully familiar with how they work, other than "typeclasses are interfaces that you can implement remotely". Since typeclasses have been proposed to Kotlin in the past without success, I assume situations similar as this were already discussed and this message won't bring them back on the table.
Also, I have heard multiple people mention context parameters could help emulate them, so maybe there is an additional context parameter solution here? But I'm not seeing it.
That's about all the options I thought of. Are there other existing ways to solve this use-case? If not, are there other potential solutions being discussed? I'm curious how this could move forward.Youssef Shoaib [MOD]
02/04/2025, 4:49 AMKProperty1
is an interface, so could you maybe have Field
and Value
implement KProperty1
? That'll take down the amount of overloads to just 4, which should be doableCLOVIS
02/04/2025, 8:01 AMKProperty1
can't be implemented in KJS 😕CLOVIS
02/04/2025, 8:14 AMKProperty1
's, so I think reusing that as the top of the hierarchy will be very confusing.CLOVIS
02/04/2025, 8:18 AMRoman Efremov
02/04/2025, 8:38 AMinterface MyAbstractValue<T> {
fun asValue(): Value<T>
}
impl MyAbstractValue<T> for Value<T> {
fun asValue(): Value<T> = this
}
impl MyAbstractValue<T> for KProperty<*, T> {
fun asValue(): Value<T> = ...
}
// and so on for all 4 types
and then, you only have to implement you operators once for MyAbstractValue<T>
type.Youssef Shoaib [MOD]
02/04/2025, 9:01 AMinterface Value<T>
interface Field<T>
interface ValueLike<in T> {
fun T.toValue(): Value<*>
}
object KotlinValueLike: ValueLike<Any?> {
override fun Any?.toValue() = TODO()
}
object KPropertyValueLike: ValueLike<KProperty1<*, *>> {
override fun KProperty1<*, *>.toValue() = TODO()
}
object FieldValueLike: ValueLike<Field<*>> {
override fun Field<*>.toValue() = TODO()
}
context(_: ValueLike<A>, _: ValueLike<B>)
infix fun <A, B> A.eq(other: B): Value<Boolean> = TODO()
// this introduces your DSL to the user, and automatically brings in the objects
fun myDsl(block: context(KotlinValueLike, KPropertyValueLike, FieldValueLike) = TODO()
However, there is no way to extract the type parameter from KProperty
, Field
etc without requiring a ValueLike
object for every such type (i.e ValueLike<F, T> { fun F.toValue(): Value<T> }
, which means you need a ValueLike<Int, Int>
, a ValueLike<KProperty1<*, Int>, Int>
, etc). This could be "fixed" (i.e. not require user to have an instance for every type) either by having Higher-Kinded types in the language, or by having some form of automatic bringing in of contexts by calling some function automatically (I can expand on what that'd look like, but Arrow had something similar sketched out)CLOVIS
02/04/2025, 9:34 AMYoussef Shoaib [MOD]
02/04/2025, 11:23 AMGiven
feature, which would allow one to summon
context parameters automatically if one isn't in scope. Using it, you could write:
interface ValueLike<F, T> { fun F.toValue(): Value<T> }
@Given
fun <T> kPropertyValueLike(): ValueLike<KProperty1<*, T>, T>
and similarly for the 3 other types, then you can have e.g.
context(_: ValueLike<F1, T>, _: ValueLike<F2, T>)
infix fun <F1, F2, T> F1.plus(other: F2): Value<T>
and this basically has the full power of type classes, but in a more Kotlin-friendly packaging (and it also allows local instances of a type class to be brought in)Alejandro Serrano.Mena
02/05/2025, 8:54 AMeq
which can take booleans and numbers, but only for the second argument, since most times the first one is already a Value
). On that note, I think that extension properties can help a lot here, so you can write User::role.v
or something similar.
On a more conceptual level, I think that Kotlin tries to emphasize explicitness in this kind of situations. For example, value classes must be constructed and deconstructed explicitly, and Kotlin lacks conversion methods (as opposed to Scala). In that regard, I would go even further than creating a single of
and have a bunch differently named arguments prop(User::score) set cond { prop(User::role) eq prop(User::candidate) }
Finally, you raise a concern about having too many overloads for solution (1). I don't think this is a problem, since checking whether those overloads match or not is relatively cheap (no complex hierarchies involved).CLOVIS
02/05/2025, 9:07 AMFor example, value classes must be constructed and deconstructed explicitly, and Kotlin lacks conversion methods (as opposed to Scala)I think that's important because one of the main use-cases of value classes is to restrain the type by adding validation in the constructor, and having an exception thrown from implicit code would be hell. However, in my case, the conversion is 100% guaranteed to succeed and be pure. I kinda feel like it would be nice to have special treatment here, but also I can't see a way to add this to the language without this feature being horribly abused everywhere.
Alejandro Serrano.Mena
02/05/2025, 9:20 AMkotlinx.html
, is to have a context in which you can perform your injection with a very simple character, such as +
or -
value {
+User::score set cond { +User::role eq +User::candidate }
.then { +1) }
.else { +User::score + 1 }
}
CLOVIS
02/05/2025, 9:26 AMof
helper but with a unaryPlus
, right? I don't think many people like the look of this though… Feels more like operator abuse than proper designCLOVIS
02/05/2025, 9:26 AMCLOVIS
02/05/2025, 8:48 PMCLOVIS
02/08/2025, 11:50 AM