gypsydave5

The blog of David Wickes, software developer

Replace when with Function Overloading

Sometimes I want a Union type of two types in Kotlin - but I can’t. Kotlin has no Union type.

Usually this can be avoided, if you have control over your types, with a Sealed Class. But if your type hierarchy has to be open, you don’t have that option.

Say we wanted a function that feeds my pet:

fun feedMyPet(pet: Animal)

But, sadly, Animal is not a sealed class. Tragically, we don’t own it. And, even worse, there’s no unified interface for feeding.

If I’ve got a Dog:

class Dog : Animal {
	fun fillUpTheDogFoodBowl()
}

And a Fish:

class Fish : Animal {
	fun sprinkleFishFoodOnTheTank()
}

Then the implementation of feedMyPet becomes harder. We can do it with a type switch:

fun feedMyPet(pet: Animal) {
	when (pet) {
		is Fish -> pet.sprinkleFishFoodOnTheTank()
		is Dog -> pet.fillUpTheDogFoodBowl()
		else -> error("I don't know how to feed a ${pet::class}")
	}
}

But the problem here is that I would like callers of feedMyPet to know that it can only work with a Fish or a Dog. I don’t want them to be surprised by runtime errors when they try to feed an Iguana:

val ivanTheIguana = Iguana()

feedMyPet(ivanTheIguana)
// => "I don't know how to feed a Iguana"

In other languages, we could introduce a union type - Fish | Dog - to limit the types that feedMyPet would accept. But Kotlin does not currently support union types.

One solution would be to create my own sealed class to represent the pets that I own:

sealed class MyPet
class MyDog(val dog: Dog) : MyPet
class MyFish(val fish: Fish): MyPet

Which would then let us get rid of the else clause.

fun feedMyPet(pet: MyPet) {
	when (pet) {
		is MyFish -> pet.fish.sprinkleFoodOnTheTank()
		is MyDog -> pet.dog.fillUpTheDogFoodBowl()
	}
}

Or even better:

sealed interface MyPet {
  abstract fun feed()
}

class MyDog(val dog: Dog) : MyPet {
	override fun feed() = dog.fillUpTheDogFoodBowl()
}

class MyFish(val fish: Fish): MyPet {
  override fun feed() = fish.sprinkleFoodOnTheTank()
}

fun feedMyPet(pet: MyPet) {
	pet.feed()
}

But this might not be the nicest interface to use:

val freddyTheFish = Fish()
val duncanTheDog = Dog()

feedMyPet(MyFish(freddyTheFish))
feedMyPet(MyDog(duncanTheDog))

If I’m having to wrap my Dog or Fish every time I want to feedMyPet, I’m going to get annoyed.

Now, maybe this is a good thing. Perhaps we’re being told that we need a new abstraction, a representation of ‘pet types that I own’ that doesn’t leak the types of Animal all over the code.

In the case of domain types (yes, like Cat and Fish here), we should take the hint and start only using MyPets everywhere.

But what if this was more incidental code that I needed to call on an ad-hoc basis? If the only time I care about MyPet is when I’m feeding it, but for the rest of the Animals life it just gets treated as an Animal, MyPet is then an irritant in the feedMyPet interface.

My preferred solution would be to use ad-hoc polymorphism, namely function overloading:

fun feedMyPet(fish: Fish) {
	fish.sprinkleFishFoodOnTheTank()
}

fun feedMyPet(dog: Dog) { 
    dog.fillUpTheDogFoodBowl()
}

This is definitely ad-hoc polymorphism - if I saw myself repeating this pattern more than once I’d most likely replace it with the sealed class.

But, when that seems like overkill, it’s a good pattern.