Making two unrelated types fit under a common abstraction?

Hi,

I don’t think I can do this (yet) in Kotlin, but sometimes when I ask I’m pleasantly surprised, so…

Say I want a function to take a list of arguments. I only want to allow Int or String (at compile time).

fun foo(???) { ... }
foo(1, 2, "a", "b") // ok
foo(1, false)       // compile-time type error

I know we don’t have union types, but in Swift I can declare a protocol P and make Int and String implement it. Then I could declare foo taking a vararg list of P.

Anything similar in Kotlin? Not the end of the world if I have to use Any, but I’m wondering.

1 Like

In some situations (including my current one) a builder would work…

buildFoo() {
    add(1, 2)
    add("a", "b", "c")
    add(false) // type error
}

The add method is overloaded to take only Int or String.

Not a solution, but still. I like type safety, and often get it by sacrificing conciseness.

I such cases (maybe, it depends) I would declare some domain-specific inline classes over String and Int, and union them via interface.
It will be verbouse to call raw->inline type conversion every time, but, again, sometimes type safety worth it.

interface Marker

inline class DomainSpecificType1(val value: String): Marker
inline class DomainSpecificType2(val value: Int   ): Marker

fun add(vararg args: Marker)

// Shortcuts to avoid boilerplate. 
// It might be a good idea to make them local 
// in some kind of builder in order to not pollute autocompletion.
val String.DST get() = DomainSpecificType1(this)
val Int.DST get() = DomainSpecificType2(this)

buildFoo {
    add("42".DST, 42.DST)
}

BTW some kotlin swagger/openapi framework (i don’t remember the name) also “solved” union problem with “marker” interfaces.

1 Like

You can’t do what you want with varargs, but fun foo(ints: List<Int>, strings: List<String>) is really not far off, and will actually be easier to call in a lot of cases.

Yes, that will be a good way sometimes. In my case at the moment I have them interleaved too much (and order matters), but I didn’t show that in my example code.

foo(1, "a", "b", 2, "c")
1 Like

The usage of “inline” in your solution gives a false impression. When used at interface level, the compiler will use boxed values, so you get the same result as without inline.
Moreover, you will need to define value on Marker, otherwise you have no means to unwrap the values inside add.

Nope the boxed versions of the classes will work as any “normal” class so you can do stuff like args[0] is DoomainSpecificType1.

Yes, of course, if you really want to do manual dispatch :slight_smile: