Extending generic interface more than once with different type parameters

As someone coming from Rust, i have grown deeply in love with the use of the Into/From traits. This has led me to want to implement something similar in Kotlin, but it has proved difficult to do so.

The main issue starts with the fact that apparently a class cannot inherent more than once from the same interface. This does not make very much sense to me, see this example:

interface Into<U> {
    fun into(): U
}

class FirstName(val firstName: String)
class LastName(val lastName: String)

class Name: Into<FirstName>, Into<LastName> {
    override fun into(): FirstName {
        TODO("Not yet implemented")
    }

    override fun into(): LastName {
        TODO("Not yet implemented")
    }
}

This as stated before gives me a compilation error but i cannot understand why? These functions have different signatures and should therefore never clash with eachother

The second issue i had is related to the reverse operation, the From trait.
While in rust we can reference a static method on a trait directly with Trait::method_name such does not seem to be possible in Kotlin without having an instance that implements that same interface.
See this example:

interface From<T, Self: From<T, Self>> {
    fun from(value: T): Self 
}
class FirstName(val firstName: String)
class LastName(val lastName: String)
class Name(val name: String): From<FirstName, Name>, From<LastName, Name> {
    override fun from(value: FirstName): Name {
        TODO("Not yet implemented")
    }

    override fun from(value: LastName): Name {
        TODO("Not yet implemented")
    }
}

Obviously this suffers from the same problem as before, but now we also can’t actually call this method unless we have an instance of Name.

I believe it is possible to get around this second issue using reflection and doing some shenanigans but this would, without a doubt, be a hack.

Am i missing something?

1 Like

Your first code sample has several problems:

  • You can’t use Into twice as a parent interface, it’s just not allowed
  • The type parameter U can never have two different values at the same time
  • You can’t distinguish methods just by return value, there must be a difference in the argument lists. While the JVM technically can distinguish the methods, Java doesn’t allow it, and Kotlin had to follow suit because of interoperability issues.

The second code sample also uses an interface twice as a parent.

So, it looks like the feature you want to implement is just incompatible to the way inheritance works in Kotlin.

4 Likes

I looked into the Rust traits, but I have to admit that I don’t see the big advantage of making possible conversions part of the type system. In Kotlin, you typically just write something like

data class User(val name: String)
data class Person(val firstName: String, val lastName: String)

fun Person.toUser() = User("$firstName $lastName")

If you really want or need to express the relationship in the type system, you can write something type-class-like (similar to Comparator):

fun interface Conversion<From, To> {
    fun From.convert(): To
}

val personToUser = Conversion<Person, User> { User("$firstName $lastName") } 

...
with(personToUser) {
    Person("John", "Doe").convert()        
}

But as you can see, it’s more or less the same as the first example, just with extra steps. Of course, now you can pass around an object of type Conversion<Person, User>, but you could also pass around a conversion function as (Person) → User instead

3 Likes

So I’m not 100% sure I understand what you mean here, but I think what you mean is that you want to be able to call the from method statically, and have it work? So you want the From interface to define a static function called from, and then every class that implements that interface has to also override that static from function?

Unfortunately that’s not really possible in Kotlin or Java. Interfaces define the behaviour of objects, but not classes.

1 Like

While in rust we can reference a static method on a trait directly with Trait::method_name such does not seem to be possible in Kotlin without having an instance that implements that same interface.

Kotlin doesn’t have inheritable statics nor self-types. However, the companion object itself can implement an interface, which is very similar:

interface From<T, X> {
    fun from(value: T), X
}

class Name(val name: String) {

    companion object : From<FirstName, Name> {
        override fun from(value: T): Name { … }
    }
}

// Usage:
val n = Name.from(FirstName("Patrick"))

Still, you won’t be able to implement the same interface twice.

Data conversion in Kotlin is almost always done via top-level extensions like:

fun FirstName.toName(): Name =
    Name(firstName)

which is a lot less code to write.

Therefore your first example would look like:

class FirstName(val firstName: String)
class LastName(val lastName: String)

class Name

fun Name.toFirstName() = TODO()
fun Name.toLastName() = TODO()
2 Likes

Here’s the thing: Rust traits, Haskell typeclasses, and Scala given are the pattern you’re describing.
The closest of these to Kotlin is the Scala approach. Scala has given declarations, which are Rust’s impl. There’s then the using modifier, which is like Rust’s trait bounds.
Kotlin doesn’t have an easy given/impl equivalent, but it does have a using/trait bound equivalent: context parameters!
In other words, Kotlin doesn’t have trait lookup (so you have to explicitly bring in your traits), but it can propagate traits implicitly to functions that need them.
The way we think about it is that you want an interface that describes what the type does, not what instances of the type do. It’s easier to see with an example:

interface Into<U, T> {
    fun T.into(): U
}
// contextual bridge function, don't worry about it too much :)
context(i: Into<U, T>) fun <U, T> T.into(): U = with(i) { this@into.into() }

class FirstName(val firstName: String)
class LastName(val lastName: String)

class Name

object NameIntoFirst: Into<Name, FirstName> { // in Rust: impl Into<FirstName> for Name
  override fun Name.into(): FirstName = TODO()
}

object NameIntoLast: Into<Name, LastName> { // in Rust: impl Into<LastName> for Name
  override fun Name.into(): LastName = TODO()
}

The nice thing is that, you don’t really need a from trait here. Into serves both purposes. If you really wanted it, you can make it, but it’s isomorphic to Into:

interface From<T, U> {
    fun from(value: T): U
}

In fact, you can just introduce a from method to Into so you can use a parameter instead of a receiver:

context(_: Into<U, T>) fun <T, U> from(value: T): U = value.into()

and even introduce a convenient type alias:

typealias From<T, U> = Into<U, T>

Now’s a good time to mention how this can be used. Here’s a function that would have trait bounds in Rust:

context(_: From<T, FirstName>, _: From<T, LastName>)
fun <T> T.decompose(): Pair<FirstName, LastName> = into<FirstName, _>() to into<LastName, _>()

Now the annoying part is how you’d use it. We need some way to get something into our context. This is achieved through the aptly named context function, which is like with, but with multiple arguments:

fun main() {
  val name: Name = TODO()
  context(NameIntoFirst, NameIntoLast) {
    val (first, last) = name.decompose()
    println("$first $last")
  }
}

That’s where the “no trait impl lookup” comes in. You have to bring in all the trait implementations explicitly. You can at least make it somewhat nicer:

inline fun <R> withNameImpls(block: context(NameIntoFirst, NameIntoLast) () -> R): R = block(NameIntoFirst, NameIntoLast)

so now you can do:

withNameImpls {
  val (first, last) = name.decompose()
}
1 Like

Building on my previous reply, we can translate Rust’s introduction to traits into Kotlin (I’ve split it into 2 parts because the run-kotlin forum block doesn’t like code that’s waaaay too long):

// trait
interface Summary<in T> {
  fun T.summarize(): String = "(Read more...)" // default impl!
}
// bridge function
context(s: Summary<T>) fun <T> T.summarize(): String = with(s) { this@summarize.summarize() }

// data
data class NewsArticle(
  val headline: String,
  val location: String,
  val author: String,
  val content: String,
)

object ArticleSummary: Summary<NewsArticle> {
  override fun NewsArticle.summarize() = "$headline, by $author ($location)"
}

data class SocialPost(
  val username: String,
  val content: String,
  val reply: Boolean,
  val repost: Boolean,
)

object PostSummary: Summary<SocialPost> {
  override fun SocialPost.summarize() = "$username: $content"
}

inline fun <R> withSummaryImpls(block: context(ArticleSummary, PostSummary) () -> R): R = block(ArticleSummary, PostSummary)

// we'll keep using this example data
val post = SocialPost(
  username = "horse_ebooks",
  content = "of course, as you probably already know, people",
  reply = false,
  repost = false,
)
val article = NewsArticle(
  headline = "Penguins win the Stanley Cup Championship!",
  location = "Pittsburgh, PA, USA",
  author = "Iceburgh",
  content = "The Pittsburgh Penguins once again are the best hockey team in the NHL.",
)

fun ex1() = withSummaryImpls {
  println("1 new post: ${post.summarize()}")
}

object NewsDefaultSummary: Summary<NewsArticle> { } // uses default impl

fun ex2() = context(NewsDefaultSummary) { // can use `with` as well, but I prefer context to be clearer
  println("New article available! ${article.summarize()}")
}

// The second version of Summary, with summarizeAuthor
interface Summary2<T>: Summary<T> {
  override fun T.summarize(): String = "(Read more from ${summarizeAuthor()})"
  fun T.summarizeAuthor(): String
}

object PostSummary2: Summary2<SocialPost> {
  override fun SocialPost.summarizeAuthor() = "@$username"
}

// personally, I'd instead implement it like this:
class AuthorSummary<T>(val summarizeAuthor: T.() -> String): Summary<T> {
  override fun T.summarize() = "(Read more from ${summarizeAuthor()})"
}
val postSummary2 = AuthorSummary<SocialPost> { "@$username" }

fun ex3() = context(PostSummary2) {
    println("1 new post: ${post.summarize()}")
}

context(_: Summary<T>)
fun <T> T.notify() = println("Breaking news! ${summarize()}")

// 2-arg notify, with different types
context(_: Summary<T>, _: Summary<U>)
fun <T, U> notifyA(item1: T, item2: U) { }
// 2-arg notify, with same type
context(_: Summary<T>)
fun <T> notifyB(item1: T, item2: T) { }

// returning something that implements a trait is a bit difficult, but we can somewhat manage
// This is basically the trait object pattern from Rust (although I came up with it independently!)
class Summarizable<T>(val value: T, val summary: Summary<T>)
// this is analogous to Box::new in Rust, although it has to be specialized to `Summary` because Kotlin doesn't have Higher-Kinded Types (if it did, we could make a generic version of the `Summarizable` class and the `summarizable()` function, but we'd still have to make the `object SummarizableSummary: Summary<Summarizable<*>>` for every trait we care about, and of course bring it in explicitly as well...
context(s: Summary<T>)
fun <T> T.summarizable() = Summarizable(this, s)
object SummarizableSummary: Summary<Summarizable<*>> {
  override fun Summarizable<*>.summarize() = summarizeImpl() 
  // using an auxillary method to get the type T to materialize
  private fun <T> Summarizable<T>.summarizeImpl() = context(summary) { value.summarize() }
}

val condition = true
fun returnsSummarizable(): Summarizable<*> = withSummaryImpls {
if (condition) article.summarizable() else post.summarizable() }

fun ex4() = context(SummarizableSummary) {
  println(returnsSummarizable().summarize())
}

fun main() {
  ex1()
  ex2()
  ex3()
  ex4()
}

Now for the Display and Formatter shenanigans:


// Making my own Display and Formatter types, just for the sake of the example
// Not using `Result` because Kotlin supports exceptions, so that's the idiomatic way to do it
interface Formatter {
  fun write(str: String)
}
context(f: Formatter) fun write(str: String) = f.write(str) // bridge
interface Display<T> {
  context(_: Formatter)
  fun T.display()
}
context(d: Display<T>, _: Formatter)
fun <T> T.display() = with(d) { this@display.display() } // bridge

// conditional trait impls are where Kotlin really falls short
// because you have to mention explicitly all the impls, and pass them to the conditional ones, etc
// it's thus very inelegant, but it works...

// Comparator<T> is already a suitable replacement for PartialOrd, so we just make a bridge method for it to get the spiffy < and > syntax
context(c: Comparator<T>)
operator fun <T> T.compareTo(other: T) = c.compare(this, other)

// Formatter implementation using StringBuilder
fun StringBuilder.asFormatter(): Formatter = object: Formatter {
  override fun write(str: String) { append(str) }
}
inline fun printFormatting(block: context(Formatter) () -> Unit) = println(buildString { block(asFormatter()) })

context(_: Display<T>, _: Comparator<T>)
fun <T> Pair<T, T>.cmpDisplay() = printFormatting {
  write("The largest member is ")
  if (first >= second) {
    write("x = ")
    first.display()
  } else {
    write("y = ")
    second.display()
  }
}

// ToString trait, but renamed method because toString() is already in Kotlin
interface ToString<T> { fun T.toString2(): String }
context(ts: ToString<T>) fun <T> T.toString2() = with(ts) { this@toString2.toString2() }
context(_: Display<T>)
fun <T> toStringFromDisplay() = object: ToString<T> {
  override fun T.toString2() = buildString { context(asFormatter()) { display() } }
}

object IntDisplay: Display<Int> {
  context(_: Formatter)
  override fun Int.display() = write("Int of $this") // use the default Kotlin toString
}

fun main() = context(IntDisplay) {
  context(toStringFromDisplay<Int>()) { // annoying nesting!
    println(3.toString2())
  }
}