-
Notifications
You must be signed in to change notification settings - Fork 361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Context receivers #259
Comments
In the section VM ABI and Java compatibility, the Kotlin code: context(C1, C2)
fun R.f(p1: P1, p2: P2) should be equal the Java code public static final kotlin.Unit f(C1 c1, C2 c2, R r, P1 p1, P2 p2) Kotlin does not allows |
First of all, let me thank you for a superb (and long-awaited) proposal. As for the discussion, my primary concern is still the syntax. The proposal mentions that the The second point is about the proposal itself. One of the important uses of multiple receivers is the ability to flexibly define behavior intersections. For example, consider this code: interface A
interface B
context(A,B) fun doSomething() It could be called as interface C: A, B
c.doSomething() This seems to be an idiomatic example of using multiple receivers and I think, it should be covered in the proposal. |
Great and thorough proposal with quite a lot of history 🙌
|
I think the proposal should go deeper on how context receivers would work with annotations and the
However, while this retrofitting doesn't happen, how would these functions be declared? There are several options: 1: @DslMarker context(JsonBuilder) fun String.by(obj: @DslMarker context(JsonBuilder) suspend () -> JsonObject) 2: context(JsonBuilder) @DslMarker fun String.by(obj: context(JsonBuilder) @DslMarker suspend () -> JsonObject) 3: context(JsonBuilder) @DslMarker fun String.by(obj: context(JsonBuilder) suspend @DslMarker () -> JsonObject) 4: context(JsonBuilder) @DslMarker fun String.by(obj: suspend context(JsonBuilder) @DslMarker () -> JsonObject) |
@mcpiroman it's already mentioned as a potential future extension. |
Another naming-related issue came to mind:
I already found code that accidentally used But it may also be an issue with |
@altavir @fluidsonic - about the suggested |
I'm not sure I understand you correctly. We're entering the ambiguous realm anyway. Consider |
@edrd-f I don't not see the problem, because we use the receiver type here exactly the same way, we would use it in any other generic case like I would prefer something even more distinct, But triangular brackets are better than round ones. |
@edrd-f please consider that |
I might've missed something, but why not put the contexts between fun <T> context(Monoid<T>) List<T>.sum(): T = ... |
The problem is, generally, the name matters the most (that's why it's before type) but with such syntax it is shifted near the end of the line. When looking at the function's definition, the context it has is more of a implementation detail, yet, you have to read it first before you get to the name or parameters, which are more important. |
to @mcpiroman's point, maybe the syntax can put the context in the next line, similar to the existing fun <T> List<T>.sum(): T
context(Monoid<T>) {
} It would follow an existing kotlin design pattern and reduce the noise in the main function definition line. Basically, the fact that the function requires additional context is a detail and thus should be below the main part of the definition. I think this would also help with discoverability on the calling side. Instead of the example |
Big +1 for the Scope Properties and Contextual Classes proposals. Scope properties would make my compiler plugins quite a bit nicer (put |
This was considered and rejected with
which I very much agree with, I don't like having parameters after the declaration. |
Great proposal, thank you! Syntax wise, in the spirit of solving the "usage of generic before its definition" I'd like to propose something perhaps slightly more verbose but that I think reads quite well and I didn't see suggested: context fun <T> with Monoid<T> List<T>.sum(): T = ... or (smaller and because context fun <T> in Monoid<T> List<T>.sum(): T = ... With multiple receivers and context suspend fun <T> with/in Monoid<T>, Scope<T> List<T>.sum(): T = ... In my opinion, it turns In type form, something like this could be used: val sum: context suspend with/in Monoid<T>, Scope<T> List<T>.() -> T However, in type form, since (if I'm not mistaken) the "usage of generic before its definition" is not a problem, I'd be in favour of keeping the originally proposed syntax (or the, imo better variant, with triangle brackets).
With this in mind, the contexts could also be moved to the end: context fun <T> List<T>.sum(): T with Monoid<T> = ... or, because this could read as "returning a context fun <T> List<T>.sum(): T given Monoid<T> = ... which would read as "returning a With multiple receivers and context suspend fun <T> List<T>.sum(): T with/given Monoid<T>, Scope<T> = ... And in type form (although, once again, the originally proposed syntax could still be used): val sum: context suspend List<T>.() -> T with/given Monoid<T>, Scope<T> All in all, I enjoy the idea of having Regardless, I just thought I'd provide my 2 cents on the syntax (as everyone enjoys doing whenever new proposals appear 😛) and I'll be happy to use whatever comes out in the end. :) |
Hello, I'm looking at the example for interface AutoCloseScope {
fun defer(closeBlock: () -> Unit)
}
context(AutoCloseScope)
fun File.open(): InputStream
fun withAutoClose(block: context(AutoCloseScope) () -> Unit) {
val scope = AutoCloseScopeImpl() // Not shown here
try {
with(scope) { block() }
} finally {
scope.close()
}
}
// usage
withAutoClose {
val input = File("input.txt").open()
val config = File("config.txt").open()
// Work
// All files are closed at the end
} However I still cannot understand how this would work IRL.
The way I imagine using this example is interface AutoCloseScope {
fun defer(closeBlock: () -> Unit)
fun close() // This does not exist in the example
}
context(AutoCloseScope)
fun File.open(): InputStream {
defer { this@File.close() } // this defer must be added for every different type that we want to autoclose, correct?
return this@File.openAsInputStream() // Didn't work with files a lot. Please accept this pseudocode
}
fun withAutoClose(block: context(AutoCloseScope) () -> Unit) {
val scope = AutoCloseScopeImpl() // Shown below
try {
with(scope) { block() }
} finally {
scope.close()
}
}
class AutoCloseScopeImpl : AutoCloseScope {
private val closeables = mutableListOf<() -> Unit>()
override fun defer(closeBlock: () -> Unit) {
closeables += closeBlock
}
override fun close() = closeables.asReversed().forEach { it.invoke() }
}
// usage
withAutoClose {
val input = File("input.txt").open()
val config = File("config.txt").open()
// Work
// All files are closed at the end
} Is my understanding correct? Do you think of a different alternative? |
The example in the text is correct. Unit-returning Kotlin functions are compiled to |
Thanks a lot for bringing up the |
That's a very interesting extension to the call resolution algorithm that we did not even consider during our design discussions. I'd love to see more specific examples in the actual code-base. So far, we've been trying to narrowly constrain the set of interfaces that are "appropriate to use as context" and it seemed to us to be pretty distinct from the set of interfaces that are "appropriate to use as an object (qualifier) of the call". However, it does not mean that the intersection is zero, and it might turn out to be useful to start the greedy resolution of the context parameters with the qualifier of the call ( |
I've answered on parentheses vs angle brackets above to @altavir. See the new Parentheses vs angle brackets section for answers.
Thanks for noting that. In fact, @ilya-g had noticed it, too, in the pre-publication review, but we failed to update the text to correct it. I've now added a clarification to the Kotlin builders section. |
@elizarov thanks for the clarification (about brackets), As for the intersection of behaviors, it was discussed a lot inside the initial KEEP-176 proposal. The case in mathematics is a simple one. Consider that you want to do some higher-level operations on matrices. You want addition, subtraction defined in A similar situation arises in other cases. For example, consider that we have a context-bound operation, that requires coroutine scope. In general, you require two receivers - a context and a coroutine scope. But the context could be a coroutine scope itself if it is an application scope or whatever. A recommendation to use an intersecting interface seems meaningless in this case since it could be the same object and could be not. Or even better, it is possible that you want to use application scope by default like: with(application){
doInContext()
} but in some cases you want to substitute the coroutinscope: with(application){
withContext(Dispatchers.IO){
doInContext()
}
} Such usage could produce some level of ambiguity in the resolution of receivers, but it was also discussed a lot in KEEP-176 and so far I see no problem because the receiver type is bound to the first appropriate receiver type it finds up the context tree. The resolution is context-aware and contexts are explicit in the function signature, so it seems fine. |
It is about visual noise, not about parsing. We strongly feel that contexts belong to the realm of "additional annotations" for a function. They are not explicitly passed on the call site and should not obscure the reading and understanding of the regular function's signature that lists all its explicit parameters that should be mentioned on the call site. That is why we recommend formatting the
|
See the answer above. We've considered something along these lines (albeit failed to mention it in the text) and rejected it because it does not lend itself to the nice multi-line formatting. |
It is all declared and used inside
Yes. That is the way we envision it, too. |
If |
@nunocastromartins I consider more readable the parameters declaration, like the context's one, before the return type. However, we need/prefer some kind of brackets?
|
@mcpiroman I think we need this functionality with any syntax, it is quite useful and I think that there are hints to it in the proposal. The problem with annotation-like markers is that they are not annotations and therefore could confuse people. Still, I am for annotation likeness since it helps a lot with my primary concern of readability and does not introduce new syntax in the language. It also is in line with what Compose does (the |
And what about lambdas? You need always remember lambdas. And they could have additional modifiers like inline or suspend. |
Because nothing exists in a vacuum. Having had to call Scala from Java, GWT from JS, and Python from C, it's welcome to see a language design for its boundaries. Not everything needs to be usable across the boundary, but that doesn't preclude you from thinking about it when designing new features. |
Because interoperability was always one of the key focus of the language. Part of the joy of working with Kotlin is the ability to interoperate easily, not only writing Kotlin code per se. Please understand that for a big part of the industry, there's no option to go with a greenfield project and have all Kotlin, so interoperating with existing code bases is a must. |
Let's have the following:
In the current state, the context will be used to create a new instance and it will be available for the entire lifetime of a class:
This design implicitly reflects the idea that a contextual parameter becomes a contextual property, and this implicitness is actually confusing. The design can be improved by adding contextual properties, e.g.
These two models have different use-cases, and they both can be expressed with the help of contextual functions only. We haven't found compelling use-cases of using contexts with classes for the first iteration, so we're now leaning towards disallowing context values for classes altogether to make a more robust decision in the next iterations. |
I meant v2 of the proposal ;) Kotlin 2.0 will bring a lot of new improvements by itself |
I agree that this can have a different interpretation based on who read it. context(Foo)
class Bar So being more explicit by moving it to the primary constructor would avoid that misinterpretation, which is aligned with something that is already working, adding any context to secondary constructors class Bar context(Foo) constructor()
I hope this is more related to the interpretation problem, but on the initial release of context receivers, they are totally supported on all constructors (only over the primary constructor is not working yet). Even if in the future for some reason the context over the class is the same as the context over the primary constructor, allowing context in the primary constructor should be valid too, to keep consistency with context over secondary constructors. |
@zarechenskiy I much prefer the |
Also, an argument can be made that, from a caller’s perspective, context parameters vs context properties are irrelevant, same as any other constructor parameter. But I’m not sure how I feel about this - constructor parameters being explicit at least requires the caller to think about it, whereas context parameters can even be passed accidentally. In the case of context properties, this seems like a recipe for bugs, since in no other situation are we intentionally storing a context as state. |
My thoughts here are that separating contextual/configuration args from functional args allows clearer intent. For instance, think about writing a database. So you may have some class, like context(Transaction)
class TableScan(predicate: (Row) -> Boolean) {} Sure, there's no reason you couldn't have |
What's the advantage over this? class TableScan(private val transaction: Transaction) {
fun execute(predicate: (Row) -> Boolean) { /* ... */ }
} |
@edrd-f You do only sometimes want to have it as an argument of a method, it's possible that this predicate is the implementation detail of this class and must be used for all clients, not the responsibility of the client code which calls it. I agree with @GavinRay97, and it's exactly how we now use context. We have helper classes for tests that are declared like
or
Nothing prevents us from passing context as an argument, but it makes it very clear where you should create those classes and just makes it possible to avoid passing context manually, reducing the amount of code, which is very important, especially for tests to avoid the unnecessary ceremony |
…ield stub around them. Following the approach here: Kotlin/KEEP#259 (comment) Next, we should adapt this to conform with the approaches in kmath, where this is largely already used.
Currently, if a function has multiple type parameters and one of them is part of the context, when calling the function with explicitly specified type parameters I have to also specify the parameter for the context. context(T)
inline fun <T, reified R> foo(block: (T) -> Unit) {
// TODO
}
fun Int.bar() {
foo<Int, String> {
// TODO
}
// or
foo<_, String> {
// TODO
}
} It would be nice to be able to define the For example as follows: context<T>(T)
inline fun <reified R> foo(block: (T) -> Unit) {
// TODO
}
fun Int.bar() {
foo<String> {
// TODO
}
} A related YT ticket is here. |
Sorry to bother, but there's been a runtime crash for two years now whenever a lambda with both extension and context receivers is inlined. Can we get this fixed soon? It's seriously limiting my ability to create DSLs with this feature. The following workarounds aren't sufficient:
|
@philipguin (it's an experimental feature, also please look up the successor issue "context parameters") |
I really like the option: extension Monoid<T> {
fun List<T>.sum(): T = ...
} and to combat the issue of that syntax being quite verbose for all the contextual use cases, forcing an additional indentation level, I suggest that a shorthand version is also introduced: extension Monoid<T> = fun List<T>.sum(): T = ... or extension Monoid<T>: fun List<T>.sum(): T = ... or ("extension fun"): extension Monoid<T> fun List<T>.sum(): T = ... for multiple context receivers: extension (Monoid<T>, Transaction) fun List<T>.sum(): T = ... with generics and inline: inline extension (Monoid<T>, Transaction) fun <reified T> List<T>.sum(): T = ... and everything else: private actual tailrec inline infix operator extension (monoid@Monoid<T>, tx@Transaction) fun <reified T : Any, E> List<T>.plus(other: List<T>): List<E> where T : Comparable<T>, E : T = listOf() |
FYI I made a project using context receivers https://github.com/MEJIOMAH17/yasb/tree/master |
I think the original idea on multiple context receivers actually really clean. For example:
The splashScreen's scope will have SplashScreen and BoxScope available and is much more understanable in my opinion instead of context() |
I'm doing some silly thing and found out code below crashes the kotlinc compiler (1.9.23) import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TestContextProperty<V>(initialValue: V) : ReadWriteProperty<Nothing?, V> {
var value = initialValue
context(Scope)
override fun getValue(thisRef: Nothing?, property: KProperty<*>): V {
return value
}
context(Scope)
override fun setValue(thisRef: Nothing?, property: KProperty<*>, value: V) {
this.value = value
}
}
class Scope {
fun <V> testProperty(initialValue: V): TestContextProperty<V> = TestContextProperty(initialValue)
}
fun scope(f: Scope.() -> Unit) {
Scope().f()
}
fun testContextReceiver() {
scope {
val a: String by testProperty("foobar")
println(a)
}
} Yes another feature interaction case need to consider : ) |
Code below which removed extends class TestContextProperty<V>(initialValue: V) {
var value = initialValue
context(Scope)
operator fun getValue(thisRef: Nothing?, property: KProperty<*>): V {
println("Something is $something")
return value
}
context(Scope)
operator fun setValue(thisRef: Nothing?, property: KProperty<*>, value: V) {
println("Something is $something")
this.value = value
}
}
class Scope(val something: String) {
fun <V> testProperty(initialValue: V): TestContextProperty<V> = TestContextProperty(initialValue)
}
fun scope(f: Scope.() -> Unit) {
Scope("something").f()
}
fun testContextReceiver() {
scope {
var a: String by testProperty("foo")
a = "bar"
println(a)
}
} So maybe crash is caused by give context receiver to |
Code below results in compiler crash. But uncomment class Context(val bar: String)
interface A {
// context(Context)
fun foo()
}
class B : A {
context(Context)
override fun foo() = println(bar)
}
fun testContextReceiverOverride() {
with(Context("foobar")) {
B().foo()
}
} |
Also in case of delegated property. Context receiver of import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TestContextProperty<V>(initialValue: V) {
var value = initialValue
context(Scope)
operator fun getValue(thisRef: Nothing?, property: KProperty<*>): V {
println("Something in $something")
return value
}
context(Scope)
operator fun setValue(thisRef: Nothing?, property: KProperty<*>, value: V) {
println("Something in $something")
this.value = value
}
}
class Scope(val something: String) {
fun <V> testProperty(initialValue: V): TestContextProperty<V> = TestContextProperty(initialValue)
}
fun scope(
something: String,
f: Scope.() -> Unit
) {
Scope(something).f()
}
fun main() {
scope("top level") {
var a: String by testProperty("foo")
scope("nested level") {
a = "bar"
println(a)
}
}
} Which output's:
Which makes more sense if instead context receiver is placed at constructor of property delegation class: context(Scope)
class TestContextProperty<V>(initialValue: V) {
var value = initialValue
operator fun getValue(thisRef: Nothing?, property: KProperty<*>): V {
println("Something in $something")
return value
}
operator fun setValue(thisRef: Nothing?, property: KProperty<*>, value: V) {
println("Something in $something")
this.value = value
}
} And also should we allow give different context receiver to single property delegation class like: import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class A
class B
class PropertyDelegation<V> {
operator fun getValue(thisRef: Nothing?, property: KProperty<*>): V = TODO()
operator fun setValue(thisRef: Nothing?, property: KProperty<*>, value: V): Unit = TODO()
context(A)
operator fun getValue(thisRef: Nothing?, property: KProperty<*>): V = TODO()
context(A)
operator fun setValue(thisRef: Nothing?, property: KProperty<*>, value: V): Unit = TODO()
context(B)
operator fun getValue(thisRef: Nothing?, property: KProperty<*>): V = TODO()
context(B)
operator fun setValue(thisRef: Nothing?, property: KProperty<*>, value: V): Unit = TODO()
}
fun main() {
val p0: String by PropertyDelegation()
with(A()) {
val p1: String by PropertyDelegation()
}
with(B()) {
val p2: String by PropertyDelegation()
}
} |
Also similar thing can already be done by using extension function. Which also captured at property declaration site instead of call site. Which is counterintuitive but reasonable. Because at call site there is nothing in property's type can tell how to pass receiver to delegation function: class TestContextProperty<V>(initialValue: V) {
var value = initialValue
}
class Scope(val something: String) {
fun <V> testProperty(initialValue: V): TestContextProperty<V> = TestContextProperty(initialValue)
operator fun <V> TestContextProperty<V>.getValue(thisRef: Nothing?, property: KProperty<*>): V {
println("Something in $something")
return value
}
operator fun <V> TestContextProperty<V>.setValue(thisRef: Nothing?, property: KProperty<*>, value: V) {
println("Something in $something")
this.value = value
}
}
fun scope(
something: String,
f: Scope.() -> Unit
) {
Scope(something).f()
}
fun main() {
scope("top level") {
var a: String by testProperty("foo")
scope("nested level") {
a = "bar"
println(a)
}
}
} |
I'm currently using context receivers, and I'd like to mention that I would love for the scope properties feature to be added. Here's an example usecase: I'm currently wrapping rendering code for a library I don't control with some more idiomatic kotlin extensions, and am adding context receivers to something. However, I need to use a context receiver from one of the properties of a super class. My currently workaround is this: interface FontHolder {
val font: Font
}
abstract class SilkScreen(title: Component) : Screen(title), FontHolder {
override val font: Font
get() = super.font
override fun render(graphics: GuiGraphics) {
with (graphics) {
rendering()
}
}
context(GuiGraphics)
open fun rendering() {}
}
// extensions
context(GuiGraphics, FontHolder)
fun drawString(text: String, x: Int, y: Int, color: Color): Int {
return drawString(this@FontHolder.font, text, x, y, color.toSRGB().toRGBInt().argb.toInt())
} with scope properties, I'd instead be able to simplify this code to abstract class SilkScreen(title: Component) : Screen(title) {
with val font: Font
get() = super.font
override fun render(graphics: GuiGraphics) {
with graphics
rendering()
}
context(GuiGraphics) // inherits the Font context from the class
open fun rendering() {}
}
// extensions
context(GuiGraphics, Font)
fun drawString(text: String, x: Int, y: Int, color: Color): Int {
return drawString(this@Font, text, x, y, color.toSRGB().toRGBInt().argb.toInt())
} and avoid the need for a useless "holder" interface, as well as simplify the code in the also, perhaps, an alternative syntax for the override fun render(graphics: GuiGraphics) with graphics {
rendering()
} or possibly override fun render(with graphics: GuiGraphics) {
rendering()
} these both ensure that the override fun render(graphics: GuiGraphics) {
methodWithoutGraphicsContext()
with graphics
codeWithGraphicsContext()
} |
Thanks everyone for the feedback. We're moving forward with the context parameters design—it's a refined version of context receivers, so I'm closing this issue and welcoming everyone in #367 |
The goal of this proposal is to introduce the support of context-dependent declarations in Kotlin, which was initially requested under the name of "multiple receivers" (KT-10468).
See the proposal text here.
The text was updated successfully, but these errors were encountered: