Improve smart casting by "dragging a value" along with a new ">:" drag operator

Consider a simple example in which smart casting doesn’t work.

class MyClass {
    var name: String? = null
	
	fun isNameEmpty(): Boolean {
		return (name != null) && (name.isEmpty() || name.toLowerCase() == "empty")
	}
}

Obviously smart casting doesn’t work in the latter part of the test and this code doesn’t compile.
One can solve this in various ways, for example with !!:

fun isNameEmpty(): Boolean {
    return (name != null) && (name!!.isEmpty() || name!!.toLowerCase() == "empty")
}

but this is rather ugly - the non-null test is repeated useslessly:
Another solution using safe calls and Elvis operators:

fun isNameEmpty(): Boolean {
    return (name != null) && (name?.isEmpty() ?: false || name?.toLowerCase() == "empty")
}

again not very pretty, optimized or readable.

The IMHO clearest and best solution is by defining a local variable for which the smart casts will work:

fun isNameEmpty(): Boolean {
    val n = name
    return (n != null) && (n.isEmpty() || n.toLowerCase() == "empty")
}

The improvement I suggest is to make this “local variable” solution available without actually spending a line of code defining the local variable.
The value that we test is dragged to the rest of the line (or the rest of the current scope) by appending a drag operator, say “>:” (but could be anything):

fun isNameEmpty(): Boolean {
    return (name>: != null) && (name.isEmpty() || name.toLowerCase() == "empty")
}

The interpretation of this would be rigorously the same as in the code above with “val n = name”.

Another example:

fun fullName(): String {
    if (name>: != null)
        return "${name.toUpperCase()} $firstName"
    else
        return ""
}

would be rigorously identical to:

fun fullName(): String {
    val n = name
    if (n != null)
        return "${n.toUpperCase()} $firstName"
    else
        return ""
}

Just having this kind of value dragging available for nullable properties of objects would be a huge benefit.

Note that the “drag operator” doesn’t break anything, it just provides a more concise way of writing the “local variable” solution.
It’s goal is similar to safe calls or the Elvis operator: code more efficiently, more concisely!

Looking forward to your feedback.

4 Likes

You can also achive that this way:

fun isNameEmpty(): Boolean {
	return name?.let { it.isEmpty() || it.equals("empty", ignoreCase = true) } ?: false 
}
9 Likes

Thanks, indeed there are many ways to achieve this (I already gave two), most of them not very elegant or overkill.
The drawback of your suggestion is that it completely changes the code.
To implement the following simple test

(name != null) && (name.isEmpty() || name.toLowerCase() == "empty")

you need a “let” + safe call + lambda + elvis:

name?.let { it.isEmpty() || it.equals("empty", ignoreCase = true) } ?: false

It actually highlights rather well the benefits of the potential “drag operator”.

2 Likes

I really like the idea. There are however 2 problems that have to be resolved. There needs to be a nice operator this (not sure I like >:, but then I don’t have a better idea right now). The other issue is the scope this applies to. You gave 2 options (the rest of the line or the rest of the current scope. I’m not sure there is a concept of a line within the compiler right now so I don’t know this is possible. Maybe something like for the rest of the statement/expression is more more feasible.
That said I prefer the other option (until the end of the current scope) in any case. I just think it is the more natural solution.

3 Likes

Could provide me then an example of the language which uses this drag operator for comparison?

I don’t know any other language that implements null safety the way Kotlin does.
It’s a Kotlin solution for a Kotlin issue.

A name suggestion: since the purpose of this operator is to fix the value of a variable within a particular scope, wouldn’t ‘fix operator’ be a better name?

(I’m not convinced of the case for one, but I’m not dismissing it yet.)

My main questions are:

  1. What scope would it apply to? Kotlin makes much less use of nested blocks than most C-like languages; in fact there isn’t even any direct syntax for a bare nested block (the braces instead indicating a lambda). — But that’s how variable declarations are scoped, so it would probably make most sense to follow that.

  2. Would it need some more special syntax to access the unfixed value inside the scope? — Again, there isn’t one for simple shadowing variable declarations, so there’s probably no need here.

This is a pretty interesting idea that I’ve never seen before, and it’s not just useful for smart casts. I have many times been annoyed that reusing a property or getter in the same expression causes another call to the getter or fetch from the class, just because the compiler can’t tell that it hasn’t changed.

I think instead of pushing the first use forward, though, it would be better to refer back to the previous ones like maybe

fun isNameEmpty(): Boolean {
    return (name != null) && (..name.isEmpty() || ..name.toLowerCase() == "empty")
}

The same syntax could refer to preceding method or property results by name like

fun isThingNameEmpty(): Boolean =
    thing.getProperty("name") == null || ..getProperty.isEmpty()

or

fun isThingNameEmpty(): Boolean =
    thing.name == null || ..name.isEmpty()

You don’t necessarily have to limit the scope to a single expression either. A .. operator could refer to any uniquely named result that is guaranteed to have been evaluated previously in the same block.

2 Likes

I like it, but I think it should be thing..getProperty and thing..name instead of just ..getProperty and ..name. That way this could be even more powerful by allowing it to work with different arguments.

println(a.foo)
println(b.foo)
println(a..foo) 
println(b..foo)
// and 
println(bar(a))
println(bar(b))
println(..bar(a))
println(..bar(b))

This of cause only works if a and b are both local values (so the compiler knows that they don’t change). Otherwise something like ..a..foo() might be possible.

I like the idea of referring back (indeed can have multiple uses!), but finding a good syntax for it won’t be easy.
Using .. will be confusing for the compiler, it’s used in ranges ; println(a..foo) could be interpreted as a range.
Maybe using 3 dots (if we can have triple double quotes why not triple dots :slight_smile:)

println(a...foo)
println(...bar(a))
return (name != null) && (...name.isEmpty() || ...name.toLowerCase() == "empty")

Don’t complicate the language.

7 Likes

Ah, I was just going to suggest using 7 dots :wink:

2 Likes

Right now I was only trying to imagine interesting ways this could be used in the language. I’m not sure it is actually something I want to see in kotlin. The main usecase for reusing the result of a function could be covered by something like const-expressions. That way the compiler could just detect that something like sqrt(x) is already known in the current scope and reuse it. Same is true for basically every other proposal introducing pure functions.

Still it’s interesting to take a concept like this drag-operator and see what can be done with it and how it would fit into the language.

How about reusing val to capture var’s value (into expression’s scope)?

return (val name != null) && (name.isEmpty() || name.toLowerCase() == "empty")
}
3 Likes

Using “val” is very similar to using a new operator like “>:” (or any other symbol combination).

I’m not sure what the right next step would be for this kind of proposal.
Who can or will decide the following questions?

  1. Is this feature useful?
  2. Is the value sufficient to warrant a new language construct (using “val”, “:>”, “…”, “…”)?
  3. Is the best implementation a forward or a backward construct?

What val would have — that new operators or whatever wouldn’t — is consistency: it looks and works much the same as it does when capturing the value in a when expression, as well as when declaring variables and properties.

That makes it that bit easier to understand, adding as little as possible for developers to learn and remember, as well as for compiler and tool writers to implement.

So yes, I think val would be significantly simpler than a new operator. Consistency is an under-rated virtue IMHO! :slight_smile:

5 Likes

This sounds very similar to the if let syntax in swift and you could just add let as a keyword or add support for a similar if val syntax.

Any kind of new cryptic syntax is definitely a showstopper for this kind of convenience feature. To pay for the all the downside of the unfamiliar syntax the new feature must be really powerful. This one does not cut it.

The val name solution is quite Kotlin-y, though. If you take Swift, where let declares a new name and if let combines a condition and a declaration, then, since Kotlin’s keyword to declare a new name is val, it becomes a total analogy in Kotlin.

However, it is not easy to design and implement the actual rules for this kind of val name feature. Introducing new declaration inside of arbitrary expressions is extremely tricky. When this kind of feature gets combined with various boolean operators (like && and ||) you enter the dark alley of flow-sensitive scoping, a place where you would not actually want to be in. It opens a door to writing a kind of code that you’d love to to never see in your life.

That’s really an issue where more actual real-life use-case need to be analyzed to find less damaging but helpful solution. Notice, that Kotin, by design, already supports shadowing. Shadowing a variable name from the outer scope with a local val name = name (using the same name) is both legal and idiomatic in Kotlin. It does take an extra line and is not DRY, so, instead of inventing some completely new syntax, we can try finding shorter but readable syntax to capture this idiom.

8 Likes

Hello Roman, thanks for your answer.
We very often use the construct val name = name which you call legal and idiomatic, but this is in fact very ugly. It’s a work-around for a language problem, not a solution.

I don’t think flow-sensitive scoping is really a big hurdle, the compiler can easily make the correct decisions and, if in doubt, use the current situation as fallback.
For example in the following:

(val name != null) && (name.isEmpty() || name.toLowerCase() == "empty")

“val name” clearly is available in the rest of the condition.
A more complex case with two "val"s - but still rather easy:

 (val name != null) && (name.isEmpty() || name.toLowerCase() == "empty") ||
 (val firstName != null) && (firstName.isEmpty() || firstName.toLowerCase() == "empty")

And the following

 (val name != null) && (name.isEmpty() || firstName.toLowerCase() == "empty") ||
 (val firstName != null) && (firstName.isEmpty() || firstName.toLowerCase() == "empty")

would obviously not compile (firstName can be null in the first line).

I guess it all depends how much you value not forcing the developer to write silly lines like val x = x.

All your examples have val name in the leading position and use && conjunction between introducing a val name and using it further down the line. Maybe it can be somehow exploited to design a better-behaving variant of this kind of a feature.