I’m in the middle of porting a largish Vert.X project from Java into Kotlin, and it’s generally a real success (coroutines ftw!), but a few things are really incredibly annoying…
For some background and as an example, the thing is a GraphQL server and one of the patterns we have is for implementing mutations, something like this:
abstract class StandardMutation<OUT, IN>(private val input: IN) {
suspend fun execute(): OUT {
basicValidation(input)
db = ...;
db.dbValidation()
return db.performMutation()
}
protected abstract fun basicValidation(input: IN)
protected abstract suspend fun DbConn.dbValidation()
protected abstract suspend fun DbConn.performMutation(): OUT
}
The problems come from the fact that the three stages can/should only communicate through fields on the object, and Kotlin is too paranoid about things:
var minAllowed: Double? = null // both are optional
var maxAllowed: Double? = null
fun basicValidation(input: ThatThing) {
minAllowed = input.minAllowed?.ensureFinite()
maxAllowed = input.maxAllowed?.ensureFinite()
// ERROR: Smart cast to 'Double' is impossible, because 'minAllowed' is a
// mutable property that could have been changed by this time
if (minAllowed != null && maxAllowed != null && minAllowed > maxAllowed)
fail("maximum allowed must be larger than minimum allowed")
// ...
}
Of course I’m able to work around these issues using !!
, lateinit
or by copying to locals and back, or whatever, but all those “solutions” seem uncharacteristically boilerplate-ish and ugly, and the errors are entirely useless in cases like these, or one might argue even incorrect (Vert.X is essentially single-threaded, and even if it wasn’t, these objects don’t go anywhere (so nothing has access to them to modify them), as they’re always used simply as new FooMutation(input).execute()
…)
So anyway, I thought I’d throw some wishes/ideas/proposals for future Kotlin versions your way:
-
create an annotation that makes the compiler ignore potential outside modifications to an object, maybe
@SingleThreaded
or@AssumeNoExternalAccess
or whatever makes most sense… given how specific the error messages are, it seems this would be really easy to implement. I’m also guessing the JavaScript mode already does this anyway? -
add a way to tell the compiler that even though a getter is custom, it will always return the same value. Perhaps it lazily computes something, or pulls something constant from elsewhere (saving the memory for an extra field), but it really is constant… maybe
val foo: T get const()
… What it would do is disable the errors like Smart cast to ‘BigDecimal’ is impossible, because ‘foo.bar’ is a property that has open or custom getter -
alternatively to the two above, transform the code during compilation so that
val
reads (within a single statement) are not actually done more than once and the values read are briefly copied to the stack, so that the above would become something like:
$stack[0] = this.minAllowed
if ($stack[0] != null) {
$stack[1] = this.maxAllowed
if ($stack[1] != null) {
if ($stack[0].doubleValue() > $stack[1].doubleValue()) {
fail("...")
}
}
}
-
(cont.) this is possibly not really feasible, i don’t know, but at least at a first glance, it seems like it wouldn’t break too many assumptions people and memory models have, but would still be NPE/CCE-safe…
-
simply downgrade the errors to warnings and/or make them opt-in with
@CompilePedantically :)