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

dave08

03/05/2024, 2:29 PM
Say I have
sealed interface Foo
and
@JvmInline value class Foo1(val value: String): Foo
and
@JvmInline value class Foo2(val value: String): Foo
I'd like to have a method on
Foo
that
Foo1
and
Foo2
implement to add a
Foo1
to another
Foo1
and a
Foo2
to another
Foo2
... at the calling site I just know it's a
Foo
, how could I implement this?
Using
fun <T : Foo> add(other: T): T
in
Foo
doesn't help too much since I don't want to be able to add a
Foo1
to a
Foo2
...
Full example:
Copy code
sealed interface Foo {
  fun add(other: ?): ?
}

@JvmInline value class Foo1(val value: String): Foo {
  override fun add...?
}


@JvmInline value class Foo2(val value: String): Foo {
  override fun add...?
}

val fooMap: Map<String, Foo> = ...

val fooMap2: Map<String, Foo> = ...

// I need to merge fooMap2 with fooMap1... each type should handle itself
And a specific key will always have the same type of Foo
Now that I think about it, it's probably not possible... so I guess I'll just need to have a big when block for each type and not let the type itself handle its own merging?
👍 1
s

Sam

03/05/2024, 2:44 PM
a specific key will always have the same type of Foo
This feels like the crucial piece of information, but there isn't a simple way to express this in Kotlin's type system. But I think you're right, a
when
statement is fine. There's no need to try and make it polymorphic if it's a sealed class. That's the whole point of sealed classes really.
c

CLOVIS

03/05/2024, 3:59 PM
I think I remember that Kotlin does not implement the function parameter super-type rule? Mathematically, this would be safe, but I don't believe Kotlin allows it:
Copy code
sealed interface Foo {
    fun add(other: Nothing): Foo
}

@JvmInline … Foo1 … : Foo {
    override fun add(other: Foo1): Foo1
}

@JvmInline … Foo2 … : Foo {
    override fun add(other: Foo2): Foo2
}
Maybe something like?
Copy code
sealed interface Foo<Self : Foo> {
    fun add(other: Self): Self
}

@JvmInline … Foo1 … : Foo<Foo1> {
    override fun add(other: Foo1): Foo1
}

@JvmInline … Foo2 … : Foo<Foo2> {
    override fun add(other: Foo2): Foo2
}
I can't try at the moment, but I think it does work. However, you can't call the interface-level method (
other
is effectively
Nothing
), since you can't prove the other argument is of the same concrete class if you don't know the concrete class of the first.
// I need to merge fooMap2 with fooMap1... each type should handle itself
This part is definitely not possible at compile-time, you can only do that with some kind of runtime check. Though with the self-type version, you could just have:
Copy code
fun Foo<*>.addAuto(other: Foo<*>) {
    when {
        this is Foo1 && other is Foo1 -> this.add(other)
        this is Foo2 && other is Foo2 -> this.add(other)
        else -> error("Both arguments should be of the same class; found $this ($this::class) and $other ($other::class)")
    }
}
d

dave08

03/05/2024, 4:56 PM
That's a pretty nice way to do it in a way that
addAuto
won't have to contain each implementation's logic... which could be nice in this case! I guess that I'm losing out on the advantage of a sealed interface's exhaustive check... but like you said, there's not much choice in the matter...
👍 1
c

CLOVIS

03/05/2024, 4:57 PM
Yeah. I would've preferred a fully static solution, but generics are erased, so as soon as it gets in a
List
you're stuck with
when
.
d

dave08

03/06/2024, 11:53 AM
In the end, since in my case a given key will never have a different type and anyways the result goes into a new Map<String, Foo>, I did this:
Copy code
sealed interface Foo {
  fun add(other: Foo): Foo = other
}

@JvmInline value class Foo1(val value: String): Foo {
  fun add(other: Foo): Foo {
    require(other is Foo1) { ... }
...
  }
}


@JvmInline value class Foo2(val value: String): Foo {
  fun add(other: Foo): Foo {
    require(other is Foo2) { ... }
...
  }
}
But I'll keep in mind your technique @CLOVIS, it could very well come in handy in other situations...
c

CLOVIS

03/06/2024, 12:38 PM
I'm not really a fan of this solution, because it's very easy to accidentally use it incorrectly 😕
k

Klitos Kyriacou

03/06/2024, 1:09 PM
Your overridden methods can return a subclass for better type safety:
Copy code
override fun add(other: Foo): Foo2
d

dave08

03/06/2024, 1:18 PM
Good point about the extra type safety... in this case it doesn't really make a difference, but nice to know!
@CLOVIS You're right... your way has the advantage of not being able to mess up... my solution doesn't mess with a bunch of generics and requires to use these function only in a certain way...
👍 1
I just wonder if it's over-design when my inputs are really very limited in this respect... covering that extra case is really something that should never come up -- I've seen a lot of times in #arrow that over-using typed exceptions when some error shouldn't happen is a no-no... so I was thinking that this is pretty similar?
c

CLOVIS

03/06/2024, 1:27 PM
It depends in your situation. IMO it's better to make invalid code not compile at all, than crash at runtime, because there's a risk it only happens in a very specific case that you forgot about. Now, of course, it's not possible to make every invalid program not compile, and the more you go that way, the more complicated the code has to be to communicate what's allowed or not to the compiler. It's up to you to decide where you put the line in the sand between simplicity and safety.
👌🏼 1