Smart casts and nullability in single-threaded contexts


#1

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:

  1. 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?

  2. 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

  3. 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("...")
        }
    }
}
  1. (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…

  2. simply downgrade the errors to warnings and/or make them opt-in with @CompilePedantically :)


Smartcast for var
Mark classes as "Not Thread Safe"
Smartcast for nullable variable properties
Smartcast for nullable variable properties
#2
  1. Use a delegate with readwritelock

Having said that, I don’t know how well you know the kotlin language, so my question is:

This are the lateinit and delegates

https://kotlinlang.org/docs/reference/properties.html#late-initialized-properties

https://kotlinlang.org/docs/reference/delegated-properties.html

I believe there is written more on this, with let if I’m right.
So then you would have something like

    let(vararg any:Any, body() - >unit) {
        Here everything passed as vararg keep their name/type but are readonly
   } 
  1. Never!!!

When these don’t apply just use local variables maybe in combination with apply / also / let / run


#3

It’s not only about threading. Your methods could do anything. For example ensureFinite() in maxAllowed line could mutate minAllowed (and compiler can’t know that in advance, because that method could be overloaded in the future).

I agree that this inspection is annoying and I don’t have a good way around it except introducing local variables.

I think that compiler could be a little bit smarter and a little bit reckless. If field is private and all called methods are private, compiler could inspect them to ensure that they can’t assign null to the variable, so it’s safe to smartcast them, and if user mutates private variable in the other thread without proper synchronizing, he’s doing it wrong anyway. But it’s a complex design to understand, it’s complex to implement and it could be a can of worms, so I’m not sure if it’s worth it.


#4

Hmm, I think you misread the example… ensureFinite() is just a stand-alone extension function

fun Double.ensureFinite(): Double {
    if (!java.lang.Double.isFinite(this)) {
       throw IllegalArgumentException("Can't use NaNs or infinities")
    } else {
        return this
    }
}

so it doesn’t have access to anything in the Mutation class at all…

But anyway, it’s not that important… I guess the overall problem I have with the current compiler behavior is this:

  1. if I implement these mutations as rather large functions (typically, there’s about 100-300 lines for each), the compiler is helpful in catching bugs and invalid assumptions
  2. if I then take such a function and refactor it into an object extending our StandardMutation (which gives me some useful functionality and DRYness regarding db transactions), where all that changes is that the code is now split in three functions and some/most of the locals become fields (though effectively they’re still just locals of the overall function from step 1), the compiler instead becomes super annoying and essentially 100% of the many errors reported are false positives

I’d strongly prefer an annotation or similar mechanism that lets me tell the compiler “look, in this case the checks you’re usually making are counter-productive”, compared to introducing a bunch of locals, lateinits, ?.lets or !!s all over the place. This is not me wanting for the compiler to just let me shoot myself in the foot, instead I want to let it know there are no guns anywhere to be seen…


#5

Well, I finished my port today… Just as a data point, the total line count of the project went from 43k to 18k, plus the code is far more readable due to using coroutines instead of callbacks, so pretty excellent.

However, I count 207 occurences of !! in there, and there’d be many more, but I only decided not to worry about using them about 3/4 of the way through… Hence why I’m complaining - in just over one week, the compiler was working against me literally hundreds of times :slight_smile:


#6

Kotlin is designed to help you prevent subtle bugs. I understand that in this case you can guarantee that it will never happen, but I am not sure if the solution is for the compiler to not use these safety features (even if told explicitly so).

If you need a lot of ?. and !!, chances are your design is not idiomatic for Kotlin. A better design will ensure that the compiler is not emitting lots of errors and that you or another developer won’t accidentally introduce a bug that Kotlin is designed to prevent in the future.

I am thinking along the lines of Optional: If one of the properties of the input is null, use a specific type to represent that. If both are present, use another. Then you will always have variables that are not nullable.

I understand that this only works well if there are only 2 possible states for the input properties to be in: complete or incomplete. If you have partially complete states, then the number of types you need will grow quickly.


#7

I understand that in this case you can guarantee that it will never happen, but I am not sure if the solution is for the compiler to not use these safety features (even if told explicitly so).

But why would that not be the solution? I mean, everyone and their mother is shouting that we shouldn’t use shared mutable state when developing multi-threaded software (with good reason). And on a platform like Vert.X, there aren’t multiple threads at all. To me, it doesn’t seem like we’re talking about some obscure case, but in fact a rather common one…

Same thing with the need for “const getters” - it’d be an extremely poor practice to have a getter that doesn’t return the same thing if called twice (without changing anything in between). Yet, it seems Kotlin assumes that all “custom or open” getters are written by insane people…

Finally, if you have a better/more idiomatic design suggestion (to replace the StandardMutation class), I’m all ears! The current design has these features:

  • it encourages you to do input validation, as you have to at least copy the values anyway (so a typical line might be this.name = input.name.trimToNull() ?: fail("name is required"))
  • it handles the transaction-related stuff for you (the actual execute() is ~20 lines), which you then don’t have to repeat (or get right) for each mutation
  • assuming the convention is followed, it allows you to do a “dry run” by simply skipping calling performMutation(), which is occasionally useful, if the API client is unable to do it (or is just lazy :slight_smile: )

#8

Correct, this is very common. But it is also common to have mutable state. And the compiler cannot determine what you are doing without completely analyzing your program and all its dependencies.

That is not true for all cases. A getter does not mean that the value never changes. It means that the value is read-only for you.

Here is an attempt, but I am afraid that based on the basic example that you give, the solution will be to simplistic:

abstract class StandardMutation<OUT, VALIDATED, IN>(private val input: IN) {
    suspend fun execute(): OUT {
        val validatedInput = basicValidation(input)
        db = ...;
        db.dbValidation(validatedInput)
        return db.performMutation(validatedInput)
    }

    protected abstract fun basicValidation(input: IN): VALIDATED
    protected abstract suspend fun DbConn.dbValidation(validatedInput: : VALIDATED)
    protected abstract suspend fun DbConn.performMutation(validatedInput: : VALIDATED): OUT
}
// Note: I would replace the 2 properties with a single property of type DoubleInterval,
// which would have the following implementations:
// - UnboundedDoubleInterval()
// - LeftBoundedDoubleInterval(minAllowed)
// - RightBoundedDoubleInterval(maxAllowed)
// - BoundedDoubleInterval(minAllowed, maxAllowed)
//
// You can then simply ask the interval if it contains a number.
//
// Creation of the interval can be done by a factory method that
// checks for `null` values and chooses the appropriate implementation.
class ValidatedInput(val minAllowed: Double?, val maxAllowed: Double?)
fun basicValidation(input: ThatThing) {
    val minAllowed = input.minAllowed?.ensureFinite()
    val maxAllowed = input.maxAllowed?.ensureFinite()

    if (minAllowed != null && maxAllowed != null && minAllowed > maxAllowed) 
        fail("maximum allowed must be larger than minimum allowed")
    // ...

    return ValidatedInput(minAllowed, maxAllowed)
}

#9

Correct, this is very common. But it is also common to have mutable state. And the compiler cannot determine what you are doing without completely analyzing your program and all its dependencies.

Yes, I realize that, and I don’t particularly mind the compiler “paranoia” being the default… What I think is sorely missing is an option of turning it off in a simple way for an entire class/file (see the “hundreds of times over one week” above)… Not only is it stressful to get so many false error reports, but it also make the code look ugly and/or needlessly verbose in the end.

That is not true for all cases. A getter does not mean that the value never changes. It means that the value is read-only for you.

I know :slight_smile: However, the compiler already has a notion of a const getter (the ‘initialize a val in constructor’ case). I’d be quite useful (again, for preventing false errors, but also as a form of documentation) if you could mark other getters as const as well…

Here is an attempt […]

I get the idea, but in addition to VALIDATED, you’d need yet another class DBDATA for sending stuff from dbValidation() to performMutation()… I understand that it would indeed silence the compiler errors, but having to create two extra classes (per mutation, so hundreds of extra classes in the entire project) to achieve that just seems like extra confirmation that something like @AssumeSingleThreadedAccess is missing in the language/compiler…


#10

I am not against anything you say (not that I would write my Kotlin code like that), but you have to come up with pretty good use cases if you want something like that to be added to Kotlin. And as this feature goes against some of the main goals: safety and maintainability, I think your chances of having this added are slim.

I also don’t understand why you don’t mind adding an annotation, but do mind adding a couple of extra classes, which are probably just a few lines more than the annotation you want to add. Yes, this will increase the number of classes to be loaded, but the number of classes in a reasonably complex Java program is already very high. The couple of hundred extra classes you add, won’t have much of an impact.


#11

Well, in my eyes, this stuff is actually making code less safe and maintainable… You can either

  • “play by the book” and write more code (e.g. these extra classes, copying stuff to locals and back, using lots of ?.let, etc). More code is harder to maintain almost by definition.
  • “know better than the compiler” and liberally use !!… but doing this a lot will make it a habit, and once it’s a habit, there will be instances where the error is actually legit, yet will be ignored.

And here, I just thought of another case to demonstrate the sillyness of it all:

class Test {
    private val lock = ReentrantLock()
    private var foo: Whatever? = null    
    fun test() = synchronized(lock) {
        foo = makeWhatever()
        if (foo.something) { 
           // ERROR: Smart cast to 'Whatever' is impossible, because yadda yadda
        }        
    }
}

The usability of Kotlin is like 3x better than Java’s in all other aspects, but for some reason, mutable nullable fields are comparatively horrible to use (and if it wasn’t for lateinit, non-null fields would be just as bad)… It just seems like such a wart on an otherwise beautiful language…

And please, there’s no need for a lecture on benefits of immutable objects - a large majority of my classes are immutable (that’s why I’d really like that const get() modifier :stuck_out_tongue:), but in the end, some state has to change somewhere, otherwise the program didn’t actually do anything…


#12

And for a good reason: The idea is that you design your Kotlin code differently than in other languages to avoid these types of fields as much as possible. If you use an external library, this is usually difficult to do. But your own code should get rid of nullable variables as soon as possible, so the rest of the code is guaranteed to always handle values that cannot be null.

Now, this does incur a “penalty”: You need to write more code than in other languages (extra local variables, extrra classes, etc.). But for me the other advantages of Kotlin far outweigh this extra bit of code.

If you feel that there are use cases where this improves the code quality a lot, I encourage you to create an issue explaining those use cases.

As far as I can tell the changes would have quite an impact: Expressions with nullable properties work differently depending on the presence of an annotation on the class. And I think that having to write (far) less code is not a good enough reason to warrant these changes. But the Kotlin developers are in a much better position to judge this.

I certainly won’t, because I see the benefits of mutability too. My example only used val properties because I assumed they would not be changed. Using var is fine too. The focus of the example was getting rid of nullable properties.


#13

How would they “work differently”?

In many ways it’s the same as why @Suppress(“UNCHECKED_CAST”) exists - you can tell the compiler that it’s ok, it did its job, and it lets you be… But, for some inexplicable reason, this is fine:

fun <T, X> cast(foo: T): X {
    @Suppress("UNCHECKED_CAST")
    return foo as X // silenced warning
}
fun test() {
    val really: IntArray = cast("abc")
}

as is this:

fun asBigDecimal(foo: String): BigDecimal {
    return foo as BigDecimal // only a warning: the cast can never succeed
}

as is this:

fun <T> asBigDecimal(foo: T): BigDecimal {
    return foo as BigDecimal // nothing!
}
fun test() {
    println(asBigDecimal("abc"))
    println(asBigDecimal(123))
}

Yet the class Test from my previous post, which is correct (unlike these three), produces an error you can’t turn off.


#14

The . without an error indication means that Kotlin guarantees that the expression before it will never be null. If you add an annotation to the class, it can suddenly be null. You said it won’t be and it probably won’t, but there is no guarantee. A developer might set it to null in another thread in the future. This happens every now and then unfortunately.

It should not be possible to change the semantics of the . operator. If I have to question the behavior of basic operators and look at multiple locations for possible annotations, it is going to be very hard for me to understand what the code is supposed to do.

Note that Kotlin is already flexible, so determining whether you are calling a function or the invocation operator, whether something is a built-in operator or an overload, etc., already requires deep knowledge of Kotlin. If the language is to remain reasonably understandable, it should not become so flexible that you can define custom behavior for everything, in my opinion.

About the cast examples: The first function uses a generic parameter as the target type of the cast, so the compiler is unable to determine what will happen there. The warning in the first asBigDecimal(...) is helpful too: The IDE has probably found a bug and is alerting you to it. The code in the second asBigDecimal(...) is perfectly valid: You are casting an object to a specific type. This may succeed or may fail with an exception, so the behavior is well-defined, the compiler assumes you know the semantics of Kotlin, and there is no need to warn you about anything.


#15

With the proposed @SingleThreaded annotation the compiler could assume that the property isn’t changed in other threads and cache it in a local variable. So if someone violates that assumption, it won’t lead to NPE; instead you just won’t see that updated null value till the end of smart cast.

But that smart cast could end very soon since any method/property invocation after the smart cast could possibly mutate the property in the same thread back to null. Thus I’m afraid this feature would be of little use in practice.

Even in this example the getter of something can mutate foo (in the synchronized block), so it might be impossible to check && foo.somethingElse after that call.


#16

The . without an error indication means that Kotlin guarantees that the expression before it will never be null.

That is simply not the case. The guarantee holds only under specific circumstances, and it took me all of 10 minutes to write warning-free Kotlin-only code that still produces NPE and also TypeCastException, UninitializedPropertyAccessException, IllegalStateException, ClassCastException and ReenteringLazyValueComputationException. There are probably other options as well, but I see no point in finding more of them…

Even in this example the getter of something can mutate foo

How exactly is that supposed to be possible? The class is not open, the property is private and it uses the default getter/setter…

But here, have a couple more examples:

class Test {
    val foo: String? = "abc" // reads from a final field
    val bar: String? get() = "def" // reads from the constant pool, which is even more constant than final fields
}

fun main(args: Array<String>) {
    val test = Test()
    if (test.foo != null) println(test.foo.length) // OK
    if (test.bar != null) println(test.bar.length) // Error
}
// this is the entire program.. there are no extra threads, the object is function-local, 
// there are no other functions, and get() isn't even called from anywhere..
fun main(args: Array<String>) {
    val timekeeper = object {
        var time: Long? = null
        fun get(): Long {
            if (time == null)
                time = System.currentTimeMillis()
            return time // but still Error
        }
    }
}

If after all these examples you still can’t accept that the compiler gets it wrong sometimes, then I see no point in any further “discussion”. If you can acknowledge that, I just wanted to let you know that in my experience, over a non-trivial amount of code, it would be very useful to have an option to silence the compiler about these two specific cases (see 1. and 2. in the initial post), and wanted to provide some encouragement towards implementing it :slight_smile:

In our codebase it happens so often, that I’m almost tempted to start doing this:

// in Java
public class TimeKeeperFields {
    protected Long time;
}
// in Kotlin
class Timekeeper : TimeKeeperFields() {
    fun get(): Long {
        if (time == null)
            time = System.currentTimeMillis()
        return time
    }
}

At least it’s just one extra class :wink:


#17

I think in this situation you’re right, since foo only transitions from null to non null value, there’s no way to set it back to null. But let’s just introduce another public method reset, which assigns null to foo, and we’re back in the situation, when it can be called from foo.something's getter.

This means that in order to prove a smart cast is possible in the body of some method, we have to analyze the bodies of all methods of that class to check there are no other reachable assignments, that can affect the smart cast. I can’t tell whether or not this problem could be solved, but if it could, it would most likely have a strong negative impact on the compile times. What also bothers me is that these proofs could become extremely fragile, so that a change in the body of one method can have a “butterfly effect” on a smart cast in another method and that makes it hard for programmer to reason about.

In that case if you want to circumvent the compiler checks and proofs entirely, you may be interested in a recent proposal about making platform types denotable in Kotlin. A platform type is the type that TimeKeeperFields.time field from Java class has in Kotlin, a nullable and non-nullable type at the same time. Feel free to submit your use cases to the discussion there.

Note that the simple case with initializing nullable time and returning it can be rewritten in a threadsafe manner and without platform types at all:

fun get(): Long = time ?: System.currentTimeMillis().also { time = it }

I hope at some point we could make an extension on a property reference that would make it possible to write it as:

fun get(): Long = ::time.getOrInit { System.currentTimeMillis() }

without an extra cost of creating the property reference itself.


#18

Please stop over-thinking this so much! I’m just asking for a way to downgrade two existing specific errors to warnings (or better yet silence). There is nothing complex to implement, the commit for this should be like < 10 lines + maybe a test…

Edit: well, that’s for the annotation thing… a const getter could easily take 20, due to syntactic addition…


#19

So I dug in a little and it turns out I was slightly too optimistic, but if I understand what’s going on in the compiler correctly, something like this might be sufficient (plus tests, of course):

So, @AssumeMultiThreadSafe would only change things in this very narrow case, where the property getter is default (a simple field read), not overridable and not from another module. @Const would indeed just bypass the whole question of mutability, but only for the one specific property where it’s used…


#20

Great, now let’s wait for the static analyzer checks that flag use of these annotations as code quality problems :wink: