is it possible to define an interface with a negat...
# getting-started
y
is it possible to define an interface with a negative bound? that is, an interface
Foo
that is not implementable by something that implements
Bar
? a static check is preferred but I'll settle with some sort of automatic runtime check
e
unless it's
sealed
you can't prevent additional implementations of
Foo & Bar
elsewhere
y
and even if it's
sealed
you can't stop someone from also implementing
Foo
for something that is
Bar
, right?
(in the same module)
e
if you have a
Copy code
sealed interface Foo
and you know that none of the implementations of
Foo
in your module are
open
to additional implementations that might implement
Bar
, I mean
y
hmm, thinking about it again I can sort of do this statically with overload resolution failure, no?
a function with a version that takes
Foo
and a version that takes
Bar
, would fail to compile.
y
This might not be perfect, but if you copy the signature of one of the methods of
Bar
, but with an incompatible return type, then seemingly it should be very hard (without using
@Suppress
) to implement both:
Copy code
interface Bar {
    fun bar() {}
}

class Bad
interface Foo {
    fun bar(): Bad = TODO()
}
//Object 'Illegal' must override 'bar' because it inherits multiple interface methods for it.
//'fun bar(): Bad' defined in '/Foo' clashes with 'fun bar(): Unit' defined in '/Bar': return types are incompatible.
object Illegal: Foo, Bar {
}
e
sure, you could
Copy code
fun negative(foo: Foo) = ...

@Deprecated(level = ERROR)
fun negative(bar: Bar)
but that wouldn't catch
Copy code
fun otherFuntion(foo: Foo) = negative(foo)
class FooBar : Foo, Bar
otherFunction(FooBar())
y
Not that though. Look at the code I wrote. I think it's genuinely impossible to implement both interfaces (I'm trying right now with
Suppress
and other things, and I can't figure it out)
y
very neat
y
However, this trick doesn't work if the method returns
Any
or
Any?
. Investigating if it's possible right now. The point is just you need to have a return type that's incompatible. This should cover most interfaces that you'd like to do this with though!
e
it would be a bit more clear as
Copy code
sealed class FooMarker
sealed class BarMarker

interface Foo {
    fun marker(): FooMarker = TODO()
}
interface Bar {
    fun marker(): BarMarker = TODO()
}
but yeah, that should prevent coincidental implementations in Kotlin (and Java) sources
it would still be possible to create implementations through other means (such as java.lang.reflect.Proxy) since they aren't actually conflicts at the JVM level
y
are the default impls there required?
e
no, it just saves you from having to implement them in subclasses
y
No, the default impls was me trying to break it somehow. It should work just the same without. The point is Kotlin normally doesn't allow you to override 2 methods with incompatible return types
y
did some testing, this seems to do exactly what I want! great!
K 2
y
I would name the return type something like
DONT_OVERRIDE_THIS_FUNCTION
to really hammer it home to the user lol!
😂 1
y
any possible issues with making
FooMarker
and
BarMarker
non-
sealed
classes and then just doing
fun marker(): FooMarker = FooMarker()
,
fun marker(): BarMarker = BarMarker()
as the default impls?
e
it's unnecessary code. I would expect it to be easier for r8 and other optimizers to see that there are no possible implementations of
FooMarker
etc. and strip out that code
but it shouldn't actually cause any problems if they were concrete classes
y
okay I love that even more now
y
Going even crazier, add some Opt-in annotations to the
FooMarker
and
BarMarker
classes. Maybe give them backticked names as well, with unicode characters inside, like
Copy code
`bro's really trying to use this type 💀`
and maybe mark the method itself with
@Deprecated(DeprecationLevel.HIDDEN)
(ooooo, and
@JvmSynthetic
)
e
can't use
@JvmName
in interfaces but those should be possible. up to you to decide if they're overkill though :p
y
You can by suppressing
INAPPLICABLE_JVM_NAME
lol. It's maybe not the best idea, but it'll make it even harder for the user to override it correctly.
e
a problem with
@Suppress("INAPPLICABLE_JVM_NAME")
is that they can lead to runtime `IncompatibleClassChangeError`s without having errors in the source code
at least that used to be the case, not sure if that's fixed now
s
Last night, I got halfway through writing an Annotation which accomplishes this. Using a custom annotation dials up the complexity quite a bit more than using built-in types, but perhaps its the right approach here. That complexity could easily be abstracted into a library.
Copy code
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class ExcludeInterface(
    val entity: KClass<*>
)

@ExcludeInterface(Vegetable::class)
interface Fruit

@ExcludeInterface(Fruit::class)
interface Vegetable

class Apple: Fruit
class Spinach: Vegetable

// Produces compile-time error via ksp symbol processor
class IllegalProduce: Fruit, Vegetable