Proposal: Platform Type Annotations While I do no...
# language-proposals
r
Proposal: Platform Type Annotations While I do not think user specifiable platform types should be added to the language grammar, I occasionally run into instances (usually involving generics) where it would be nice if my Kotlin wrappers around platform code could specify they work with platform types, possibly using an annotation. Example in thread.
I have a small library that allows me to use JavaFX with more idiomatic Kotlin code. In that library I have the following helper functions:
Copy code
public operator fun <T> ObservableValue<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

public operator fun <T> WritableValue<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
    this.value = value
}
This allows code such as the following
Copy code
// SimpleObjectProperty is a Java class provided
// by JavaFX that is a basic implementation of
// ObservableValue and WritableValue used above
val colorProperty = SimpleObjectProperty<Color>()
var color by colorProperty
However, this will say that
color
is a
Color
, but due to how JavaFX works, it really should be
Color!
. It would be nice if this could be annotated some how, preferably on the operators in the library, but if not, at least on the property or delegate in locale code.
y
How is
SimpleObjectProperty
defined? I think there could be a way to pass-on the fact that Color may be nullable
r
Preferred solution:
Copy code
public operator fun <T> ObservableValue<T>.getValue(
    thisRef: Any?,
    property: KProperty<*>,
): @PlatformType T = value

public operator fun <T> WritableValue<T>.setValue(
    thisRef: Any?,
    property: KProperty<*>,
    value: @PlatformType T,
) {
    this.value = value
}
Or perhaps even
Copy code
public operator fun <@PlatformType T> ObservableValue<T>.getValue(
    thisRef: Any?,
    property: KProperty<*>,
): T = value

public operator fun <@PlatformType T> WritableValue<T>.setValue(
    thisRef: Any?,
    property: KProperty<*>,
    value: T,
) {
    this.value = value
}
Less preferred, but still usable solutions:
Copy code
val colorProperty = SimpleObjectProperty<@PlatformType Color>()
or
Copy code
var color: @PlatformType Color by colorProperty
@Youssef Shoaib [MOD] Sure, I could make it nullable by saying
SimpleObjectProperty<Color?>
, but that's really a good solution for the same reason that Kotlin just marking everything from the platform as nullable isn't a good solution.
y
I mean, as far as I can tell, someone can just pass
SimpleObjectProperty<Nothing>
and bad things happen. How does
SimpleObjectProperty
work? If one can provide e.g. a property reference, then you could use that to smuggle a platform type thru I think
Your issue is that
SimpleObjectProperty
spawns things from thin air, so things can always go wrong. If it was e.g.
SimpleObjectProperty(SomeJavaFxClass::color)
, then I think it'll end up with a platform type and everything will work fine
r
I'm not 100% sure I understand what you're asking.
SimpleObjectProperty
is a Java class (part of the JavaFx framework). It doesn't spawn anything from anywhere. It just stores a value and has useful functions for observation. Because it's a Java class, I can't guarantee the nullability of the stored value.
y
Ohhhhhh okay that makes more sense
r
Sorry, I guess I should have said that when I used it instead of just including it out of the blue. My bad.
y
What happens if you just write:
Copy code
public operator fun <T> ObservableValue<T>.getValue(thisRef: Any?, property: KProperty<*>) = value // or getValue()
r
I believe that works, but has two annoying limitations: 1: My library currently uses explicit API mode, and I would have to remove that in order to use your suggestion (which would be a negative for everywhere else IMAO) 2: It doesn't apply to the setter, since I have to specify the type in the function parameter For the first one, perhaps that's my fault and I shouldn't be using explicit API mode when wrapping a Java library, but that seems dubious to me. For the second, I'm not sure how much of a concern that is, as Kotlin seems to take the type from the getter and ignores the setter entirely, but I'm not sure at the moment what that means for
color::set
(i.e. is that a
(Color) -> Unit
or a
(Color!) -> Unit
)
There is also a compounding issue as well. In my library I also have functions to convert JavaFX property bindings between types that just call JavaFX functions that do the same, but with the arguments in a more idiomatic Kotlin order (to allow trailing lambdas). For example:
Copy code
public fun <T> ObservableValue<T>.intBinding(
    vararg dependencies: Observable,
    func: (T) -> Int,
): IntegerBinding = createIntegerBinding({ func(value) }, this, *dependencies)
// createIntegerBinding is a static Java function from JavaFX
Without the ability to specify platform types somehow, I have to make the
T
arg to the
func
param either nullable or not, which again doesn't align with the underlying assumptions.
y
This is a little convoluted, so bear with me:
Copy code
import kotlin.reflect.KProperty

// This can exist in its own module, with explicit API turned off. It only needs to exist once
sealed interface TypeWrapper<T> {
    companion object IMPL : TypeWrapper<Nothing>
}

@Suppress("UNCHECKED_CAST")
public fun <T> platformType() = PlatformType.identity(TypeWrapper.IMPL as TypeWrapper<T>)
//////////////////////


class SimpleObjectProperty<T>
public operator fun <T> SimpleObjectProperty<T>.getValue(thisRef: Any?, property: KProperty<*>): T = TODO()

public operator fun <T> SimpleObjectProperty<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
    
}
fun <T> SimpleObjectProperty(type: TypeWrapper<T>): SimpleObjectProperty<T> = TODO()

fun example() {
    val foo by SimpleObjectProperty(platformType<String>()) // IntelliJ shows type as String!
    foo.length // compiles fine!
    foo!! // no warnings!
}
Copy code
import org.jetbrains.annotations.NotNull;

public class PlatformType {
    @NotNull
    public static <T> TypeWrapper<T> identity(TypeWrapper<T> type) {
        return type;
    }
}
This creates a platform type from a user-provided normal type. The key is that the user needs to use
platformType
to specify the type. You can also have the
SimpleObjectProperty
automatically have a platform type by explicitly defining (with explicit API turned off):
Copy code
fun <T> SimpleObjectProperty() = SimpleObjectProperty(platformType<T>())

fun example() {
    val foo by SimpleObjectProperty<String>() // IntelliJ shows type as String!
    foo.length // compiles fine!
    foo!! // no warnings!
}
so every platform type introduction will need some explicit-API-prohibited method, but after that type has been smuggled, it can be used wherever you'd like just fine. Is there error suppression for explicit API?
r
That's not a great solution in my case as I don't only want to affect
SimpleObjectProperty
, but any observable or writable values. There are many different implementation of those interfaces (
ObservableValue
and
WritableValue
) including anonymous object implementations common for the JavaFX CSS integration, so I couldn't override them all if I wanted to.
> Is there error suppression for explicit API? No, there doesn't appear to be, but even if there were, that wouldn't solve the "compounding" issues I mentioned.
y
Slightly better idea:
Copy code
@Suppress("UNCHECKED_CAST")
private fun <T> SimpleObjectProperty<*>.asType(type: TypeWrapper<T>) = this as SimpleObjectProperty<T>
public fun <T> SimpleObjectProperty<T>.toPlatformType() = asType(platformType<T>())

fun example() {
    val foo by SimpleObjectProperty<String>().toPlatformType() // IntelliJ shows type as String!
    foo.length // compiles fine!
    foo!! // no warnings!
}
Make such a
toPlatformType
for every interface that's used. It's annoying, but it's only a one-time cost
r
It's an interesting solution, but I don't think it's a great general purpose solution for two reasons: 1: While my example use case is with JavaFX, Java isn't the only platform type interop that Kotlin has to deal with, so relying on the
PlatformType.java
class doesn't always work 2: Unless I'm missing something, it appears to still only addresses the property delegation calls, and not any other uses (such as the binding extension example I gave)
y
For the binding one, as long as
ObservableValue
was converted to a platform type
ObservableValue<T!>
, then the lambda should have the right type
r
Ah, I see. There is still the issue that I can't do that for all cases, such as with anonymous classes defined in external libraries (e.g. user created custom controls), but I'm not sure how much of a concern that will be in this case.
Do you think such work arounds are sufficient to address this concern in the general case, or would it still be worth some sort of platform type declaration annotation? I'm sure JavaFX isn't the only platform library where this can come up, and having to create a
PlatformType.whatever
class equivalent for whatever platform you're targeting is an annoying bit of boilerplate in an otherwise Kotlin only project.
y
PlatformType
class only is for your private implementation to easily create `TypeWrapper`s of platform types. You can make that private, or write it once in a library. The actual cost per type is this pattern:
Copy code
@Suppress("UNCHECKED_CAST")
private fun <T> SimpleObjectProperty<*>.asType(type: TypeWrapper<T>) = this as SimpleObjectProperty<T>
public fun <T> SimpleObjectProperty<T>.toPlatformType() = asType(platformType<T>())
Which can probably be generated automatically. The point then is the user chooses to use a platform type by writing
toPlatformType
, and call resolution takes care of finding the right one. You need 2 such methods for every type you want to support, but subtypes are fine btw, except that they'll be cast to whatever supertype has
toPlatformType
defined on it. So you'd define it for
ObservableValue
,
WriteableValue
, etc, and the user just needs to use it when possible.
r
Good point