Kotlin has no support for typeclasses. We’re trying out multiple approaches to see what is possible.

Our (example) problem

We have a class Coordinate:

data class Coordinate(val x: Int, val y: Int) {
    operator fun plus(c: Coordinate): Coordinate = Coordinate(x + c.x, y + c.y)
}

We also want to support coordinates with big numbers, or decimals. To do so, we can make the number type variable:

data class Coordinate<T>(val x: T, val y: T) {
    operator fun plus(c: Coordinate<T>): Coordinate<T> = Coordinate<T>(x + c.x, y + c.y)
}

This does not work. + is not defined on generic type T. Let’s look at different ways to handle this.

1. Make Coordinate an interface with implementations for every Number type

interface Coordinate<T> {
    val x: T
    val y: T
    operator fun plus(c: Coordinate<T>): Coordinate<T>
}

data class CoordinateInt(override val x: Int, override val y: Int): Coordinate<Int> {
    override fun plus(c: Coordinate<Int>): Coordinate<Int> =
        CoordinateInt(x + c.x, y + c.y)
}

data class CoordinateDouble(override val x: Double, override val y: Double): Coordinate<Double> {
    override fun plus(c: Coordinate<Double>): Coordinate<Double> =
        CoordinateDouble(x + c.x, y + c.y)
}

val i: Coordinate<Int> = CoordinateInt(0, 4) + CoordinateInt(1, 2)
val d: Coordinate<Double> = CoordinateDouble(0.1, 0.5) + CoordinateDouble(0.1, 0.4)

This works well enough. However, I cannot instantiate a Coordinate using just Coordinate(1, 2).

This is fixable, by adding extension functions on the companion object of Coordinate:

    interface Coordinate<T> {
        val x: T
        val y: T
        operator fun plus(c: Coordinate<T>): Coordinate<T>
        companion object // This needs to exist for extension functions on Coordinate.Companion to work
    }

    data class CoordinateInt(override val x: Int, override val y: Int): Coordinate<Int> {
        override fun plus(c: Coordinate<Int>): Coordinate<Int> =
            CoordinateInt(x + c.x, y + c.y)
    }
    fun Coordinate.Companion.create(x: Int, y: Int): Coordinate<Int> = CoordinateInt(x, y)

    data class CoordinateDouble(override val x: Double, override val y: Double): Coordinate<Double> {
        override fun plus(c: Coordinate<Double>): Coordinate<Double> =
            CoordinateDouble(x + c.x, y + c.y)
    }
    fun Coordinate.Companion.create(x: Double, y: Double): Coordinate<Double> = CoordinateDouble(x, y)

    val i: Coordinate<Int> = Coordinate.create(0, 4) + Coordinate.create(1, 2)
    val d: Coordinate<Double> = Coordinate.create(0.1, 0.5) + Coordinate.create(0.001, 0.4)
    //This does not compile. Coordinate.create(Long, Long) doesn't exist
//    val l: Coordinate<Long> = Coordinate.create(1L, 2L)

This is probably good enough for any practical purpose. But let’s continue looking at other options anyway, for science!

Doing this using a typeclass

In another language that does have typeclasses (Scala 3 for this example), we could use a typeclass Addable. Something like this:

(the functions are called add here to not interfere with existing plus or + functions)

trait Addable[A]:
  extension (a: A) def add(other: A): A

given Addable[Int] with
  extension (v: Int) def add(other: Int): Int = v + other

given Addable[Double] with
  extension (v: Double) def add(other: Double): Double = v + other

case class Coordinate[T: Addable](x: T, y: T) {
  def add(c: Coordinate[T]): Coordinate[T] =
    Coordinate(x.add(c.x), y.add(c.y))
}

val i: Coordinate[Int] = Coordinate(0, 4) `add` Coordinate(1, 2)
val d: Coordinate[Double] = Coordinate(0.1, 0.5) `add` Coordinate(0.001, 0.4)
//This does not compile. Addable[Long] doesn't exist.
//val l: Coordinate[Long] = Coordinate(1L, 2L)

A bit cleaner, because now we’re defining how to add the numbers in their own typeclass instances, instead of in a Coordinate class.

Also, we see now that Coordinate can be an addable too. Let’s model it like that:

trait Addable[A]:
extension (v: A) def add(other: A): A

given Addable[Int] with
extension (v: Int) def add(other: Int): Int = v + other

given Addable[Double] with
extension (v: Double) def add(other: Double): Double = v + other

case class Coordinate[T: Addable](x: T, y: T)

given coordinateAddable[T: Addable]: Addable[Coordinate[T]] with
extension (v: Coordinate[T]) def add(c: Coordinate[T]): Coordinate[T] =
Coordinate(v.x.add(c.x), v.y.add(c.y))

val i: Coordinate[Int] = Coordinate(0, 4) `add` Coordinate(1, 2)
val d: Coordinate[Double] = Coordinate(0.1, 0.5) `add` Coordinate(0.001, 0.4)
//This does not compile. Addable[Long] doesn't exist.
//val l1: Coordinate[Long] = Coordinate(1L, 2L)
val c: Coordinate[Coordinate[Double]] = Coordinate(Coordinate(0.1, 0.5), Coordinate(0.1, 0.5)) `add` Coordinate(Coordinate(0.1, 0.5), Coordinate(0.1, 0.5))

Now we can even use Coordinates composed of Coordinates. I’m not sure what this means, but that’s how we roll with functional programming!

And, if we really want to, we can lift the restriction on Coordinate, so we can make them with non-Addables:

trait Addable[A]:
extension (v: A) def add(other: A): A

given Addable[Int] with
extension (v: Int) def add(other: Int): Int = v + other

given Addable[Double] with
extension (v: Double) def add(other: Double): Double = v + other

case class Coordinate[T](x: T, y: T)

given coordinateAddable[T: Addable]: Addable[Coordinate[T]] with
extension (v: Coordinate[T]) def add(c: Coordinate[T]): Coordinate[T] =
Coordinate(v.x.add(c.x), v.y.add(c.y))

val i: Coordinate[Int] = Coordinate(0, 4) `add` Coordinate(1, 2)
val d: Coordinate[Double] = Coordinate(0.1, 0.5) `add` Coordinate(0.001, 0.4)
//This does compile now. Coordinates can be made of non-Addables, the resulting Coordinate is however not Addable
val l1: Coordinate[Long] = Coordinate(1L, 2L)
//This does not compile. Extension add on Coordinate[Long] doesn't exist.
//val l: Coordinate[Long] = Coordinate(1L, 2L) `add` Coordinate(1L, 2L)

2. extending Number

Back to Kotlin. What can we do to emulate typeclasses?

We could only allow subclasses of Number, and implement add on Number:

fun <T: Number> Number.add(other: T): T = when(other) {
    is Int -> other + this.toInt()
    is Double -> other + this.toDouble()
    else -> throw IllegalArgumentException("cannot add unknown number $other")
} as T

data class Coordinate<T: Number>(val x: T, val y: T) {
    infix fun add(c: Coordinate<T>): Coordinate<T> = Coordinate<T>(x.add(c.x), y.add(c.y))
}

val i: Coordinate<Int> = Coordinate(0, 4) add Coordinate(1, 2)
val d: Coordinate<Double> = Coordinate(0.1, 0.5) add Coordinate(0.1, 0.4)
//This compiles, but throws an exception at runtime
val l: Coordinate<Long> = Coordinate(0L, 4L) add Coordinate(1L, 2L)

This looks simple and it works. But it’s not great. We have to do a lot of type casting, and it will break at runtime instead of compile time.

3. extension functions

data class Coordinate<T>(val x: T, val y: T)

@JvmName("CoordinatePlusInt")
infix fun Coordinate<Int>.add(other: Coordinate<Int>): Coordinate<Int> =
    Coordinate(x + other.x, y + other.y)
@JvmName("CoordinatePlusDouble")
infix fun Coordinate<Double>.add(other: Coordinate<Double>): Coordinate<Double> =
    Coordinate(x + other.x, y + other.y)

val i: Coordinate<Int> = Coordinate(0, 4) add Coordinate(1, 2)
val d: Coordinate<Double> = Coordinate(0.1, 0.5) add Coordinate(0.001, 0.4)
//This does not compile. add is not defined for Coordinate<Long>
//    val l: Coordinate<Long> = Coordinate(1L, 2L) add Coordinate(1L, 2L)

fun List<Coordinate<Int>>.addAll(): Coordinate<Int> = reduce { c1, c2 -> c1 add c2 }
//This does not compile. add is not defined on all Coordinates<T>
//    fun <T> List<Coordinate<T>>.addAll(): Coordinate<T> = reduce { c1, c2 -> c1 add c2 }

This is a lot like option 1, but will allow any T. The resulting Coordinate will only be addable for specifically defined classes. Which might either be useful, or inconvenient.

4. Addable wrappers

We can add an Addable interface, with wrapper value classes.

interface Addable<T> {
    val value: T
    infix fun add(other: Addable<T>): Addable<T>
    companion object
}

@JvmInline
value class AddableInt(override val value: Int) : Addable<Int> {
    override infix fun add(other: Addable<Int>): Addable<Int> =
        AddableInt(value + other.value)
}
fun Addable.Companion.create(v: Int): Addable<Int> = AddableInt(v)

@JvmInline
value class AddableDouble(override val value: Double) : Addable<Double> {
    override infix fun add(other: Addable<Double>): Addable<Double> =
        AddableDouble(value + other.value)
}
fun Addable.Companion.create(v: Double): Addable<Double> = AddableDouble(v)

@JvmInline
value class AddableCoordinate<T>(override val value: Coordinate<T>) : Addable<Coordinate<T>> {
    override infix fun add(other: Addable<Coordinate<T>>): Addable<Coordinate<T>> =
        AddableCoordinate(value add other.value)
}
fun <T> Addable.Companion.create(v: Coordinate<T>): Addable<Coordinate<T>> = AddableCoordinate(v)

data class Coordinate<T>(val ax: Addable<T>, val ay: Addable<T>) {
    fun x() = ax.value
    fun y() = ay.value
    override fun toString(): String = "Coordinate(${x()}, ${y()})"
    infix fun add(c: Coordinate<T>): Coordinate<T> = Coordinate<T>(ax add c.ax, ay add c.ay)
    companion object
}
fun Coordinate.Companion.create(x: Int, y: Int): Coordinate<Int> = Coordinate(Addable.create(x), Addable.create(y))
fun Coordinate.Companion.create(x: Double, y: Double): Coordinate<Double> = Coordinate(Addable.create(x), Addable.create(y))
fun <T> Coordinate.Companion.create(x: Coordinate<T>, y: Coordinate<T>): Coordinate<Coordinate<T>> = Coordinate(Addable.create(x), Addable.create(y))

val i = Coordinate.create(0, 4) add Coordinate.create(1, 2)
val d = Coordinate.create(0.1, 0.5) add Coordinate.create(0.1, 0.4)
val c = Coordinate.create(i, i) add Coordinate.create(i, i)
val cc = Coordinate.create(c, c) add Coordinate.create(c, c)
//does not compile, create does not exist
//    val l: Coordinate<Long> = Coordinate.create(0L, 4L) add Coordinate(Addable.create(1L), Addable.create(2L))

This works, but it’s quite cumbersome to add an extra number implementation and to deal with all the wrappings.

5. Addable function in Coordinate

What if we just supply the add function to Coordinate?

data class Coordinate<T>(val x: T, val y: T, val addF: (T, T) -> T) {
    override fun toString(): String = "Coordinate($x, $y)"
    infix fun add(c: Coordinate<T>): Coordinate<T> = Coordinate<T>(addF(x, c.x), addF(y, c.y), addF)
    companion object
}
fun Coordinate.Companion.create(x: Int, y: Int): Coordinate<Int> = Coordinate(x, y, Int::plus)
fun Coordinate.Companion.create(x: Double, y: Double): Coordinate<Double> = Coordinate(x, y, Double::plus)
fun <T> Coordinate.Companion.create(x: Coordinate<T>, y: Coordinate<T>): Coordinate<Coordinate<T>> =
    Coordinate(x, y, Coordinate<T>::add)

val i = Coordinate.create(0, 4) add Coordinate.create(1, 2)
val d = Coordinate.create(0.1, 0.5) add Coordinate.create(0.1, 0.4)
val c = Coordinate.create(i, i) add Coordinate.create(i, i)
val cc = Coordinate.create(c, c) add Coordinate.create(c, c)
//does not compile, create does not exist
//    val l = Coordinate.create(0L, 4L)

We have to supply the add function ourselves. Unlike real typeclasses which can be summoned by type. This is not a big problem here, because we can make these create extension functions to simplify this.

This seems to be very much in the spirit of typeclasses. And it’s also the easiest to make, use and understand.

For this solution all T have to be addable. If we also want to support non-addable Coordinates, solution 3 makes more sense.

Conclusion

Good we tried this. Not only did we find a better solution, we also learned about typeclasses, and found out that we might want to support Coordinates of Coordinates and non-addable Coordinates.

This code can also be found here.

shadow-left