Hi, might something like adding interfaces to type...
# language-proposals
f
Hi, might something like adding interfaces to types at compile-time ever be implemented? It would be really handy at simplifying and deduplicating e.g. extension functions with overloads or improving readability and simplicity. We could do something like the following:
Copy code
interface Taggable {
    fun addTag(tag: Tag)
}

/*
 * Example syntax for interfaces with abstract methods
 */
extend String : Taggable {
    override fun addTag(tag: Tag) {
        // ... implementation 
    }
}
Or this for marker interfaces:
Copy code
interface Primitive

/* 
 * Shorthand syntax for extending multiple types with a "marker" interface (has no methods to override)
 */
extend String, Byte, Short, Int, Long, Float, Double : Primitive
For example, Int would now have these supertypes: Number, ComparableInt, Serializable and Primitive
y
This is the typeclass pattern from other languages like Haskell, Scala, and Rust, also known as "ad-hoc polymorphism" Kotlin has a decent alternative: contexts! I have a nice write-up here, translating Rust's introduction to traits into Kotlin To quote myself:
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.
For your case:
Copy code
// you want an interface that describes what the type does, not what instances of the type do
interface Taggable<in T> {
  fun T.addTag(tag: Tag)
}
// bridge function
context(t: Taggable<T>)
fun <T> T.addTag(tag: Tag) = with(t) { this@addTag.addTag(tag) }

// implementation
object StringTaggable: Taggable<String> {
  override fun String.addTag(tag: Tag) {
    // ... implementation
  }
}

// utility
context(_: Taggable<T>)
fun <T> T.addABunchOfTags() {
  addTag(Tag1)
  addTag(Tag2)
  ...
}

// use site
context(StringTaggable) {
  "foo".addABunchOfTags()
  "bar".addTag(Tag1)
}
and:
Copy code
interface Primitive<in T>

// implementations
object StringPrimitive: Primitive<String>
object BytePrimitive: Primitive<Byte>
...

// convenience function
inline fun <R> withPrimivites(block: context(Primitive<String>, Primitive<Byte>, ...) () -> R): R = block(StringPrimitive, BytePrimitive, ...)

// utility
context(_: Primitive<T>, _: Primitive<R>)
fun <T, R> doSomethingWithPrimitives(foo: T, bar: R) = ...

// use site
withPrimitives {
  doSomethingWithPrimitives("foo", 54.toByte())
}
m
Copy code
interface Foo {
    fun f()
}

extend Int: Foo {
    override fun f() {}
}

fun g(foo: Foo) {
    foo.f()
}
I second your proposal. This just makes sense.
Copy code
interface Foo<in T> {
    fun T.f()
}

context(impl: Foo<T>)
fun <T> T.f() {
    with(this) self@{
        with(impl) { this@self.f() }
    }
}

object FooInt: Foo<Int> {
    override fun Int.f() {}
}

fun <T> g(foo: T, fooImpl: Foo<T>) {
    context(fooImpl) {
        foo.f()
    }
}
This is dreadfully verbose and obtusely roundabout. It also makes me want a compiler plugin to automatically generate the helper bridge methods. I’ve grown very fatigued by how every little trivial thing requires enormous plugin support.
y
IMO such an `extend`s system is just difficult to design, especially on the JVM. Scala went from having such a system (ish, they had an implicit wrapping mechanism that'd wrap your objects so they kinda implement new interfaces) to doing a context-like design. I can go into more detail if you'd like; in short, the illusion that a type implements some interface when that's not actually the case on the JVM is easily shattered; not sure about other platforms, but I imagine they may face similar problems. Kotlin has enough niceties (extension functions, contexts) to give us this context-based alternative without too much compromising on syntax. FWIW, your code example can be simplified a bit:
Copy code
interface Foo<in T> {
    fun T.f()
}

context(impl: Foo<T>)
fun <T> T.f() = with(impl) { f() } // or `with(impl) { this@f.f() }` to be safer

object FooInt: Foo<Int> {
    override fun Int.f() {}
}

context(_: Foo<T>)
fun <T> g(foo: T) {
    foo.f()
}
and only the bridge function sticks out really Maybe there's a world where the above has nicer syntax sugar:
Copy code
typeclass Foo { // implicitly has `T`
  fun foo() // implicitly has receiver, and generated bridge
}

object FooInt: Foo<Int> {
    override fun Int.f() {}
}

fun g(foo: Foo) { // generates an overload or similar that has the type parameter shenanigans
    foo.f()
}
m
It’s a matter of taste but I don’t consider extensions functions and contexts to be niceties because they make name resolution very indirect and destroy local reasoning and composability. But I understand I can’t have typeclasses in Kotlin for technical reasons.
y
In this case it definitely composes well to give you the syntax you want though Off-topic, but I'd argue contexts are actually pretty composable and help with local reasoning; name resolution is a bit indirect, sure, but sometimes you genuinely don't need to know exactly which overload of a function is getting called (e.g. in DSLs). Also, wouldn't this have the same issues?
Copy code
interface StringExt {
  fun splice(...)
}
extend String : StringExt {
  override fun splice(...) = ...
}

"Hello world".splice(...)
Looks like extensions with extra steps 🤷🏼
m
I find this kind of language discussions somewhat pointlessly hypothetical. I just wanted to second the proposal of the OP.
name resolution is a bit indirect, sure, but sometimes you genuinely don't need to know exactly which overload of a function is getting called (e.g. in DSLs).
But that’s because the complex name resolution is a solution looking for a problem. The same mechanism would mostly be dead weight in other languages. Saying ‘(e.g. in DSLs)’ is a bit misleading because DSLs, which I don’t use, are kind of the only significant justification.
Looks like extensions with extra steps 🤷🏼
Yes, because you chose to add the extra steps.
Copy code
fun splice(text: String, /* ... */) {/* ... */}

splice("Hello, world!", /* ... */)
Also, the point of the proposed
extend
feature is defining an
interface
that can be implemented for existing types.
Copy code
interface Foo {/* ... */}

extend Int: Foo {/* ... */}
extend String: Foo {/* ... */}
extend Boolean: Foo {/* ... */}

fun f(foo: Foo)
To enable calls such as
f(42)
,
f("")
, and
f(true)
.
y
Apologies, I'm not communicating too clearly. I meant that
extend
can be (mis)used to end up with extension functions. This isn't just hypothetical; Scala 2's `implicit`s were used as such, too; in Scala 3, they replaced it with (equivalents of)
context
and extension functions Haskell has type classes that look a lot more like
extend
. They run into issues when there are multiple reasonable implementations of a typeclass, and you wish to switch between them. Their solution is to wrap them in (their equivalent of) `value class`es. `context`s let you choose the local instance you want to use. If anything, `context`s are more explicit than typeclasses are in Haskell and Scala because you have to provide them locally (i.e. a context must come from a lexically enclosing scope), while those other languages have involved algorithms that assemble such a typeclass, i.e. they replace complex name resolution with typeclass resolution (in Scala's case, they have both!) I support the core idea of the proposal (adding "interfaces" to types at compile-time), but IMO,
context
and extension functions would need to be incorporated into such a design because they serve similar purposes.
m
I meant that
extend
can be (mis)used to end up with extension functions.
I don’t consider that misuse and would prefer that to extension functions. The problem of having to repeat the API can be solved by making
extend
support adding instance methods as well as implementing interfaces.
Copy code
extend Int {
    fun negate(): Int { return -this }
}
extend Int: Foo {
    override fun f() {/* ... */}
}
An
extend<T, This: Bar<T>> This
block would be more concise and less brittle than repeating
fun <T, This: Bar<T>> This
for each extension function you wish to define on
This
too.
They run into issues when there are multiple reasonable implementations of a typeclass, and you wish to switch between them.
More often than not, there’s a reasonable implementation to default to IME. Personally I find it not worth bothering to optimise your design for the 2–30% edge case.
j
> I don’t consider extensions functions and contexts to be niceties because they make name resolution very indirect and destroy local reasoning and composability Could you please clarify why? I actually feel the opposite. With contexts, the declaration site is super clear. The use site requires a matching instance in the local scope, so quite local, just like
CoroutineScope
. With the proposed interface extensions, it's only worse: the fact that you can use a value as another interface is defined in some completely unrelated place (that you probably would have to import), and the import is nowhere to be found in the local lexical scope of the use site. How is this better than contexts, where the context instance has to be in the local scope of the use site?
f
I agree that if this system is to be implemented, interface extensions should behave similarly to typealises and extension functions - being included in the file imports (so only one per package per type is allowed). Maybe with a syntax like this:
import extension my.simple.example.String
or
import extension my.simple.example@kotlin.String
This would already signal to the writer that the type is extended in some way, especially if they import it manually. And, if not, they'd probably be using an IDE, which would somehow present it anyways.
the fact that you can use a value as another interface is defined in some completely unrelated place (that you probably would have to import), and the import is nowhere to be found in the local lexical scope of the use site.
Yes, I also agree in this regard. My original intention was mainly to simplify calling functions and passing types as arguments without the need to declare a new function for every type. As @min said, then we would be able to do this:
Copy code
interface Foo {/* ... */}

extend Int: Foo {/* ... */}
extend String: Foo {/* ... */}
extend Boolean: Foo {/* ... */}

fun f(foo: Foo)

f(1)
f("")
f(true)
without the need to declare three different functions. I might just be misunderstanding your arguments, but from my perspective, contexts only clutter the call site and make it less readable.
m
@Joffrey If Henry Ford had asked people what they wanted, they would have said faster horses. A lot of developers would also tell you that they want
fun T.f()
, but what is it that people might actually be seeking? The biggest difference between that and
fun f(x: T)
is that the extension method enables use in method call chains. When I declare an extension method for that reason, what I actually want is just better piping such as
x |> f(y) |> g(z)
to call
g(f(x, y), z)
.
Copy code
interface Foo {
    fun T.f(y: U)
}
interface Bar {
    fun f(x: T, y: U)
}
I also find it misleading that
x.f(y)
in a
Foo.() -> Unit
makes the function appear binary when it takes three runtime arguments. That also prevents
T::f
or
x::f
which has been a source of friction for me. It’s doubly misleading if the extension method doesn’t read to or write from, and shouldn’t have been an instance method under,
Foo
at all. I see many such cases where an API declares not semantic instance methods as a name resolution hack. I prefer to call
bar.f(x, y)
in a
(bar: Bar) -> Unit
because it spells out all three runtime arguments, and declaring such a function give you a clearer opportunity to consider its namespace.
Copy code
fun androidComposeView(): AndroidComposeView { return TODO() }

fun g0(observer: LifecycleObserver) {/* ... */}
fun g1(calculator: PositionCalculator) {/* ... */}
fun g2(source: LayoutSource) {/* ... */}

g0(androidComposeView())
g1(androidComposeView())
g2(androidComposeView())
Lastly, does any of these calls surprise you? You miss the point of using a compiled language. Without familiarising yourself with every type in a framework and what classes and interfaces each extends, type safety lets you take it at face value that
AndroidComposeView
extends
LifecycleObserver
,
PositionCalculator
, and
LayoutSource
by virtue of compiling. None of the three is listed directly as a supertype, and in fact, I made up the last interface. You could still command + click to jump to the definition of
LayoutSource
and find all its implementors, the same way as the other two, including the necessary
extend AndroidComposeView: LayoutSource
block if you wanted to. Not having to shove everything about a type in its declaration lets you place an interface implementation where the private utilities it depends on are visible for example, and other languages with this feature work fine without your imagined problems.
Copy code
interface Bar<Self> {
    fun Self.f()
    fun Self.g()
    fun Self.h()
}

object BarInt: Bar<Int> {
    override fun Int.f() {}
    override fun Int.g() {}
    override fun Int.h() {}
}

object BarString: Bar<String> {
    override fun String.f() {}
    override fun String.g() {}
    override fun String.h() {}
}

context(impl: Bar<T>)
fun <T> T.f() {
    with(this) self@{
        with(impl) { this@self.f() }
    }
}
context(impl: Bar<T>)
fun <T> T.g() {
    with(this) self@{
        with(impl) { this@self.g() }
    }
}
context(impl: Bar<T>)
fun <T> T.h() {
    with(this) self@{
        with(impl) { this@self.h() }
    }
}

context(impl: Bar<T>)
fun <T> go(bar: T) {
    bar.f()
    bar.g()
    bar.h()
}

context(BarInt) { go(42) }
context(BarString) { go("") }
It’s one thing to say that this is the best that’s feasible on the JVM for technical reasons.
Copy code
interface Bar {
    fun f()
    fun g()
    fun h()
}

extend Int: Bar {
    override fun f() {}
    override fun g() {}
    override fun h() {}
}
extend String: Bar {
    override fun f() {}
    override fun g() {}
    override fun h() {}
}

fun go(bar: Bar) {
    bar.f()
    bar.g()
    bar.h()
}

go(42)
go("")
Refusing to recognise the ergonomics of this design is just unserious.
j
I might just be misunderstanding your arguments, but from my perspective, contexts only clutter the call site and make it less readable.
@Fellter sorry I wasn't really making an argument against this proposal, but merely trying to understand @min’s argument. It seemed to me that the argument about local reasoning is one that would go in favor of contexts instead of in favor of this proposal, so I was confused and tried to explain why I don't get it.
@min thanks a lot for the additional details! > The biggest difference between that and fun f(x: T) is that the extension method enables use in method call chains. When I declare an extension method for that reason, what I actually want is just better piping such as x |> f(y) |> g(z) to call g(f(x, y), z). That is one benefit of extensions, but definitely not the most important to me. IMO the great thing about extensions is the ability to declare function that feel like they are part of the original API, and don't differ syntactically from regular member functions. From the caller's perspective, there isn't supposed to be a difference with members, and that's why I like that they look the same. This is already useful without any chain.
path.exists()
reads much better IMO than
Paths.exists(path)
. By the way, please don't be fooled by the
[JetBrains]
tag. While I do work at JetBrains, I'm not part of the Kotlin team, so I'm just commenting as a fellow user here 🙂
➕ 1
I also find it misleading that x.f(y) in a Foo.() -> Unit makes the function appear binary when it takes three runtime arguments.
Do you feel the same about
this
anywhere? That part might be a matter of habit. Now that said, I'd like to point out that you're no longer talking about extensions in general but more specifically member extensions that have a dispatch receiver and that are exposed outside the type they are declared in. I do share some of your concerns here, and I think contexts are slightly better to express this because a little bit more explicit.
I prefer to call bar.f(x, y) in a (bar: Bar) -> Unit because it spells out all three runtime arguments, and declaring such a function give you a clearer opportunity to consider its namespace.
I think it really depends on the specific semantic of each of the parameters. I think having the version where
Foo
is a dispatch receiver (or context) is better if: 1.
f
"acts on"
x
2.
f
could have been a member of
x
, at least if x had been defined in this specific project 3. `Foo`/`Bar` merely adds context to the call that is not directly related to what's happening (e.g. logger, analytics reporter, diagnostics, some sort of execution context, etc.), or if it brings constraints to the call that are not about the call itself but a wider section of code (e.g.
coroutineScope { ... }
).
➕ 1
> other languages with this feature work fine without your imagined problems Wow, sorry I don't quite get your point here. I was really just trying to make sense of your argument. You were talking about contexts breaking local reasoning, and I just pointed out that this proposal is worse in this respect, because it seems even less local and explicit than contexts. I'm not saying I myself am against the proposal, but there are definitely costs to readability due to the fact this would create a case where the type hierarchy is no longer contained in declarations. > Refusing to recognise the ergonomics of this design is just unserious Where did I do that? First, I was merely pointing out that the drawback you mentioned for contexts exists here too. I never mentioned what I think about the ergonomics benefits of the proposal. I think a part the drawbacks of this proposal are similar to extension functions: they are also defined separately from the original type, yet used as if they were part of it. They are not locally visible, they require an import (I think there is a proposal to improve this). I do like extension functions, as I said earlier, and I found their drawbacks worth the benefits. I do see some value here in this similar proposal, though I wouldn't jump straight on it because we have to consider the whole, pros/cons, how that design impacts and interacts with other features etc. One thing that would concern me is how extension interfaces would play with
is
.