Fellter
10/15/2026, 8:07 PMinterface 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:
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 PrimitiveYoussef Shoaib [MOD]
10/15/2026, 8:17 PMThe closest of these to Kotlin is the Scala approach. Scala hasdeclarations, which are Rust’sgiven. There’s then theimplmodifier, which is like Rust’s trait bounds.using
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.
Youssef Shoaib [MOD]
10/15/2026, 8:26 PM// 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:
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())
}min
10/16/2026, 6:37 AMinterface Foo {
fun f()
}
extend Int: Foo {
override fun f() {}
}
fun g(foo: Foo) {
foo.f()
}
I second your proposal. This just makes sense.
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.Youssef Shoaib [MOD]
10/16/2026, 6:56 AMinterface 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:
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()
}min
10/16/2026, 7:47 AMYoussef Shoaib [MOD]
10/16/2026, 7:57 AMinterface StringExt {
fun splice(...)
}
extend String : StringExt {
override fun splice(...) = ...
}
"Hello world".splice(...)
Looks like extensions with extra steps 🤷🏼min
10/16/2026, 8:16 AMname 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.
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.
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).Youssef Shoaib [MOD]
10/16/2026, 8:33 AMextend 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.min
10/16/2026, 8:49 AMI meant thatI don’t consider that misuse and would prefer that to extension functions. The problem of having to repeat the API can be solved by makingcan be (mis)used to end up with extension functions.extend
extend support adding instance methods as well as implementing interfaces.
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.
Joffrey
10/16/2026, 8:54 AMCoroutineScope.
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?Fellter
10/18/2026, 4:33 PMimport 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:
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.min
10/19/2026, 7:33 AMfun 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).
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.
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.
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.
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.Joffrey
10/19/2026, 7:59 AMI 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.
Joffrey
10/19/2026, 8:19 AMpath.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 🙂Joffrey
10/19/2026, 8:25 AMI 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 { ... }).Joffrey
10/19/2026, 8:26 AMis.