3

Let's say I have sealed class I'm using for a server response:

sealed class Response{
    class Success: Response() 
    class ErrorA: Response() 
    class ErrorB: Response() 
}

And a bogus response:

fun getResponse(): Response{
    val r = Random()
    return when (r.nextInt(3)) {
        0 -> { Response.Success() }
        1 -> { Response.ErrorA() }
        2 -> { Response.ErrorB() }
        else -> { throw IllegalStateException() }
    }
}

And I want to handle the response. I currently could use something like this:

fun handle(response: Response) = when (response) {
    is Response.Success -> { handle(response) }
    is Response.ErrorA -> { handle(response) }
    is Response.ErrorB -> { handle(response) }
}

Which the compiler will then ensure handles all cases. An awesome feature!

Why, though, could I not do something like this:

class ResponseHandler(){

    fun handle(success: Response.Success) {}

    fun handle(error: Response.ErrorB) {}

    fun handle(error: Response.ErrorA) {}
}

and call

ResponseHandler().handle(response)

This achieves the same thing but does not compile, my question is this: in the same way that the compiler ensures, at runtime, that all cases are handled in a when statement, why can the same logic not be applied to method overloading?

Any information or referrals to further reading would be hugely helpful. Thanks

0

3 Answers 3

2

This problem can be broke down to this simplified example:

fun calc(i: Int) = i * 2
fun calc(d: Double) = d * 2

fun main(args: Array<String>) {
    val i: Number = 5
    calc(i)
}

You have two specialized methods that take an Int and Double respectively. Your value is of type Number (supertype of both, Int and Double). Although i obviously is an integer, your variable has a type Number, which cannot be an argument to either calc(i: Int) or calc(d: Double).

In your case, you get a Response and want to invoke one of the overloaded methods, none of which takes a Response directly.

Sign up to request clarification or add additional context in comments.

1 Comment

That makes sense to me, but isn't a sealed class a special case? Because the sealed class itself is abstract but the subtypes are known at compile time? Specifically, sealed classes let the compiler ensure that all subtypes are handled at compile time by a when statement. This does not apply to inheritance in general, so there must be an explicit mechanism for sealed classes. What is the mechanism used by the compiler for ensuring that, and is there a technical reason that mechanism couldn't be used for overloading (outside of whether it would be good language design or not)?
2

In principle it could be done (essentially by auto-generating the handle(response: Response) = when ... method). But I don't think it's ever likely to be. Overloading in Kotlin works basically the same as in Java/Scala/other JVM languages and introducing a major difference for so little benefit doesn't looks like a good idea (of course this doesn't apply to when which is Kotlin-specific).

If you want it, you can just define the same fun handle(response: Response) inside ResponseHandler (and make the other handle methods open so it's actually useful).

Comments

0

For anybody curious about this, I spent quite a long time trying to find a language with the dynamic behavior I described in my question, but while maintaining a real type system. Two years after asking this question I came across Julia, and learned that the phrase which describes what I was trying to achieve is multiple dispatch.

My question originated with the capabilities provided by sealed class. This is essentially "sum types" and allows the compiler to guarantee that each subtype is covered in a when expression. It indeed a compile-time feature.

Multiple Dispatch is a feature which achieves the following: when a method is called, instead of determining the function to call at compile time, the language determines at runtime which function most appropriately matches the types. In other words, it runs the function which best matches the arguments.

Kotlin does not have multiple dispatch, and this can be demonstrated by the following:

object Dispatcher {
    fun add(a: Int, b: Int) {
       // ...
    }

    fun add(a: Float, b: Float) {
       // ...
    }

    fun add(a: Float, b: Int) {
       // ...
    }
}

fun main(){
    val numbers = listOf<Number>(1, 2.0)
    Dispatcher.add(numbers[0], numbers[1])
}

Attempting to run this causes the following compile-time error:

None of the following functions can be called with the arguments supplied:
public final fun add(a: Float, b: Float): Unit defined in Dispatcher
public final fun add(a: Int, b: Float): Unit defined in Dispatcher
public final fun add(a: Int, b: Int): Unit defined in Dispatcher

Despite the values of numbers[0] and numbers[1] having the types Int and Float respectively, the compiler can not know this at compile time, and so add(a: Int, b: Float) will not be called.

Contrast this with a language which supports multiple dispatch. Julia is one such language, and in fact makes multiple dispatch a central feature. The above example can be implemented in Julia like so:

julia> function add(a::Int, b::Int)
           println("add Int to Int")
       end

julia> function add(a::Float64, b::Float64)
           println("add Float to Float")
       end

julia> function add(a::Int, b::Float64)
           println("add Int to Float")
       end

Calling add then looks like this:

julia> numbers = Number[1, 2.0]
2-element Vector{Number}:
 1
 2.0

julia> add(numbers[1], numbers[2])
add Int to Float

Julia identifies which method to call at runtime.

As with all programming language design features, each choice comes with a set of trade-offs, often suitable for different contexts. Kotlin and Java are able to have extensive tooling integration because types can be determined at compile time. Julia is able to be a much more dynamic language with higher expressiveness, but sacrifices some static analysis.

1 Comment

An example of a language with a much stricter type system than Julia, but with support for multiple dispatch would be C# actually. Using the dynamic type, you can cast any type to a dynamic type, after which at runtime the correct function overload will be resolved. In the C# realms this is sometimes referred to as "late binding".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.