https://kotlinlang.org logo
#getting-started
Title
# getting-started
k

kierans777

03/17/2022, 2:44 AM
Hi. Does Kotlin support runtime polymorphism for functions?
Copy code
open class Animal(val name: String)
class LandAnimal(name: String, val legs: Int) : Animal(name)
class SeaAnimal(name: String, val flippers: Int) : Animal(name)

fun printAppendages(animal: SeaAnimal) {
    print(" has ${animal.flippers} flippers")
}

fun printAppendages(animal: LandAnimal) {
    print(" has ${animal.legs} legs")
}

fun printAppendages(animal: Animal) {
    print(" has no appendages")
}

fun <T : Animal> printAnimal(animal: T) {
    print(animal.name)
    
    printAppendages(animal)
    
    print("\n")
}

fun main() {
    val lassie = LandAnimal("Lassie", 4)
    val nemo = SeaAnimal("Nemo", 2)
    
    printAnimal(lassie)
    printAnimal(nemo)
}
Copy code
Lassie has no appendages
Nemo has no appendages
Or is polymorphic dispatch only for methods?
j

Joffrey

03/17/2022, 7:29 AM
Overloads are resolved at compile time based on the declared type of their arguments. This applies to top level functions, but also methods. Polymorphism only applies to the receiver of the methods (the instance on which you call the method)
t

Tim Oltjenbruns

03/17/2022, 12:26 PM
If you absolutely need a separation of operation and data, then in Java (and Kotlin) most people’s first pick is Visitor pattern. Alternatively, you can accomplish the same effect with sealed classes and extension methods. But before you go down either of those roads, I would consider whether you could make the operation part of the objects, such as giving all animals a list of appendages that can be named.
Copy code
class Appendage(val name: String)

open class Animal(val name: String, val appendages: List<Appendage>)
// or alternatively if not all animals have appendages
open class AppendagedAnimal(name: String, val appendages: List<Appendage>) : Animal(name)

// or if all appendages on one animal type have the same name
class Appendages(val name: String, val count: Int)
class Animal(val name: String, val appendages: Appendages? = null)
Back to Visitor/Sealed classes w/extensions. If you have a good reason to separate the operation from the object hierarchy, Both of these patterns require a stable object hierarchy, because using this pattern makes it easy to add operations while making it painful to add new elements. For some excellent docs on how and when to use Visitor, see here. For the alternate version using sealed classes and extension methods, I will provide a quick example.
Copy code
sealed class Animal(val name: String)
class LandAnimal(name: String, val legs: Int) : Animal(name)
class SeaAnimal(name: String, val flippers: Int) : Animal(name)
class OtherAnimal(name: String) : Animal(name)

fun Animal.printAppendages() {
    when (this) {
        is LandAnimal -> printAppendages()
        is SeaAnimal -> printAppendages()
        is OtherAnimal -> printAppendages()
    }
}

fun SeaAnimal.printAppendages() {
    print(" has $flippers flippers")
}

fun LandAnimal.printAppendages() {
    print(" has $legs legs")
}

fun OtherAnimal.printAppendages() {
    print(" has no appendages")
}

fun main() {
    val lassie = LandAnimal("Lassie", 4)
    val nemo = SeaAnimal("Nemo", 2)

    lassie.printAppendages()
    nemo.printAppendages()
}
I think in many cases the sealed class and extension method pattern accomplishes Visitor pattern, without some of the weirdness. You still accomplish the goal, separating the operation from the object hierarchy. And because the objects are marked sealed, its marked in code that “this object structure is hard to change, and should remain stable”
For reference, the traditional Visitor pattern option:
Copy code
open class Animal(val name: String) {
    open fun accept(visitor: AnimalVisitor) {
        visitor.visit(this)
    }
}
class LandAnimal(name: String, val legs: Int) : Animal(name) {
    override fun accept(visitor: AnimalVisitor) {
        visitor.visit(this)
    }
}
class SeaAnimal(name: String, val flippers: Int) : Animal(name) {
    override fun accept(visitor: AnimalVisitor) {
        visitor.visit(this)
    }
}

interface AnimalVisitor {
    fun visit(animal: Animal)
    fun visit(landAnimal: LandAnimal)
    fun visit(seaAnimal: SeaAnimal)
}

class PrintAppendages : AnimalVisitor {
    override fun visit(animal: Animal) {
        print(" has no appendages")
    }

    override fun visit(landAnimal: LandAnimal) {
        print(" has ${landAnimal.legs} legs")
    }

    override fun visit(seaAnimal: SeaAnimal) {
        print(" has ${seaAnimal.flippers} flippers")
    }

}

fun main() {
    val lassie = LandAnimal("Lassie", 4)
    val nemo = SeaAnimal("Nemo", 2)

    PrintAppendages().visit(lassie)
    PrintAppendages().visit(nemo)
}
k

kierans777

03/17/2022, 11:24 PM
@Tim Oltjenbruns Thanks for all of that. I am familiar with the Visitor pattern and solving this problem using OOP/polymorphism. However the program I'm writing is Functional, and thus inserting an OOP style visitor pattern in would be clunky. The classes are really data classes/structures they don't hold behaviour. Perhaps there's a feature request here for Kotlin, to have polymorphic function dispatch like Clojure does.
t

Tim Oltjenbruns

03/18/2022, 12:08 AM
Yeah, not quite like Clojure, but you can accomplish dynamic dispatch with a little bit of boilerplate with that extension method
57 Views