Aria Pahlavan
Aria Pahlavan

Reputation: 1408

Smart-cast and comparison inside When Expression after 'is' type-check

According to the documentation for When Expression, it can replace "if-else if", so I tried implementing a function to return maximum of two variable of Any type:

fun maxOf(a: Any, b: Any) = when {
    a is Int && b is Int        -> if (a < b) b else a
    a is Double && b is Double  -> if (a < b) b else a
    a is Int && b is Double     -> if (a < b) b else a
    a is Double && b is Int     -> if (a < b) b else a
    a is String && b is String  -> if (a < b) b else a
    else                        -> null
}

The above implementation works but I thought it could be more concise:

fun maxOf(a: Any, b: Any) = when {
    (a is Int || a is Double) && (b is Int || b is Double) -> if (a < b) b else a
    a is String && b is String                             -> if (a < b) b else a
    else                                                   -> null
}

But I failed because the second implementation doesn't work; the error is in the first occurrence of if (a < b):

Unresolved references.

None of the following candidates is applicable because of receiver type mismatch

public fun String.compareTo(other: String, ignoreCase: Boolean =...): Int defined in kotlin.text

Is this because smart-cast is not capable of casting a and b to their actual types after evaluating the expression (a is Int || a is Double) && (b is Int || b is Double)? Or am I missing something?

Update

The same error happens even if the type of a and b is changed to Number:

fun maxOf(a: Number, b: Number) = when {
    (a is Int || a is Double) && (b is Int || b is Double) -> if (a < b) b else a
    else                                                   -> null
}

Upvotes: 3

Views: 662

Answers (1)

ice1000
ice1000

Reputation: 6569

Why this happen?

It's not a problem of when expression. You should blame on the type system.

a and b are instance of Int and Double, so Kotlin will infer them as their LCA. For example:

open class A { fun a() = println("meow meow meow") }
class B : A()
class C : A()
if (a is B || a is C) a.a() // smart cast works

However, Int and Double are subclasses of Number and Comparable at the same time, and Kotlin doesn't know if you want a Number or a Comparable, so Kotlin treat it as an instance of Any.

if (a is Int || a is Double)
  if (a > 1) print("meow meow") // smart cast doesn't work

This is why the "bug" appears.

How to fix this?

Use your original code, or use explicit cast (I know it sucks, but it is really an unavoidable problem).

I've came up with a beautiful solution! Look at it:

fun <T : Comparable<T>> maxOf(a: T, b: T): T? = when {
    (a is Int || a is Double) && (b is Int || b is Double) -> if (a < b) b else a
    a is String && b is String -> if (a < b) b else a
    else -> null
}

And in this way, you can apply any Comparable to it (instead of Int, Double and String only)!

fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a < b) b else a

Your original version will return null if input is invalid, and this version will raise compilation error if input is invalid, helping you find error at compile time.

Mention!

You've commented me and asked a further question. I want to make my reply more readable so I'll add it to my answer.

fun someFunction(a: Number) {
    if (a is Int || a is Double) println(a < 1) // still error! Why?!
}

Yeah, indeed, a is an instance of Number here. But please do mention, <, or, compareTo, is not declared in Number. It's in Int or Double :).

Edit 2

You said:

using generics forces a and b to be the same type.

So try this:

fun <A : Comparable<B>, B : Any> maxOf(a: A, b: B): Any = if (a < b) b else a

And you said:

explicit cast is not a solution if the type of a and b is Any

  • solution zero: create operator fun Any.compareTo
  • solution one: Give up casting.

Upvotes: 3

Related Questions