-
Notifications
You must be signed in to change notification settings - Fork 362
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 parameters #367
Comments
The use of First thing that comes to mind for me would be like a |
@JakeWharton Not that is probably matters but |
With the introduction of named context parameters, it feels like |
Indeed. Also it's uncommon enough to not be taken by any other framework in the ecosystem ( |
I can suggest Another alternative could be to overload the new |
I read the document and have several points to mention:
|
Based on the example as far as i understand,
One more note is about the DI. I can't understand how would this help with DI. DI is more like "here are the tools you need to do your job" while context parameters read (to me) like "i have the tools, you can borrow them to do what you need to " Nevertheless, i like the progress. |
You aren't borrowing them from anywhere. You already own the references. They were explicitly supplied to the function, just contextually rather than nominally (like a normal parameter). A borrow also deeply implies a lifetime to the reference which doesn't really apply (at least no more than one would think of a named parameter as a borrowed reference). |
Some of the DI use cases are already doable with just primary receiver without using any context parameter/receivers, For example, class ApplicationScope(
private val store: Store,
private val httpClient: HttpClient
)
class Logger {
fun ApplicationScope.log(message: String) {
this@ApplicationScope.store.add(message)
}
} Here the scope can be provided with with(application){
logger.log(message)
}
logger.log(message) // will fail to compile I guess context parameters makes this |
Has there been any discussion of simply disallowing unnamed context parameters? Unless I'm mistaken, this would eliminate...
I'm guessing everyone commenting on this issue is going to be an experienced Kotlin user -- who else is reading a technically complex KEEP and then feeling like they can express their opinion about it -- but I am trying to think about this feature from someone who is brand new to Kotlin. Attempting to wear the newbie hat, I think if I ran across the following code, I'd find it very unsightly and confusing: interface Logger {
context fun log(message: String)
fun thing()
} To explain this code to that new user, you'd have to explain a lot to them at once -- what context parameters are, why some are unnamed, and how method visibility / propagation works for these unnamed context parameters (which is unique and different from the rest of the language). "Well you see, yeah, here "thing" is kind of public, but then "log" is really public. I mean, for "thing", you need to summon it..." The implication of this small code snippet worries me too: value class ApplicationContext(
val app: Application
) {
context fun get() = app.get()
// copy of every function, but with context on front
} How much boilerplate are codebases going to need to introduce if they want to support unnamed context parameters? Maybe someone can clue me into a use-case where unnamed context parameters are really important. Are they really worth propagating so much noise through the rest of the language for them? Will this be a lot of burden on library developers, who now have to think about every class they write in the context of being used as an unnamed context parameter? Or maybe I'm being totally obtuse, and you'd still need to support this syntax even if every context parameter were named? |
Even though I see the value and flexibility in the contextual visibility section, its complexity is really over the rest of the features of the language for me. With a little knowledge of programming (basic level) and English, you can understand Kotlin code more or less easily, but the contextual visibility will drop this "readability" a lot. I prefer the simpler approach it had before in which if you are in a specific context, you have access to all of its public properties/functions, not the only ones marked as |
Some answers and clarifications:
|
Maybe this was not entirely clear from the document, but the contextual visibility only affects to context receivers, not named context parameters, which work like a regular parameter in terms of accessibility. We think that the default mode of use of this feature should actually be named context parameters, which are must simpler to explain, pose no problem for visibility, and do not require using It's even debatable whether interface Logger {
fun log(message: String)
}
// how to use it
context(logger: Logger) fun User.doSomething() = ... Our intention is in fact to make the library author think what is the intended mode of use of the type being defined. Not every class is suitable to be thrown as context receiver directly, since it (potentially) pollutes the scope with lots of implicitly-available functions. Why have context receivers, then? There are several (interrelated) reasons, which in many cases boil down to the same distinction between value parameters and extension receivers we already have in Kotlin.
We took this concern quite seriously during the design. Our point of view, however, is that this problem is not that big.
|
Having processed my thoughts a bit more, and talking with some friends, I can see the value in an unnamed context parameter, so I partially retract my previous comment. (For example, a class can be used as a control scope; that can be pretty neat.) I am even growing to support the I still think context functions are a misstep. It feels like a bunch of complexity which opens up a lot of confusing design decisions for code authors and can produce code that just looks confusing to read and wrap your head around (a class with a mix of At this point, I'd rather that unnamed context parameters did not populate the current method scope at all. If you want to use a method on an unnamed context parameter, then just context(Session, Users, logger: Logger)
fun User.delete() {
val users = summon<Users>()
users.remove(this)
logger.log("User removed: ${user.id}")
// Or use `with(summon<Users>) { ... }` if you really want to populate the scope with its methods
}
// Or maybe just name the `Users` parameter instead? Wouldn't this approach prevent the need for |
Here fun main() {
val a = A()
context(3) { a.f() }
}
fun A.g() = context(1) { f() } |
First of all, I really appreciate the introduction of named context parameters. I am still trying to realize the other changes. As I understand, the a) it marks a callable as accessible from an unnamed context receiver To me, these two purposes look completely unrelated and it feels like they should be separated.
|
Why holding to the design with separate // here with is a keyword (a hommage to the typeclasses KEEP)
fun myAwesomeContextFunction(x: Int, with logger: Logger) {
logger.log(x)
}
with(Logger()) {
myAwesomeContextFunction(1) // log: 1
myAwesomeContextFunction(2, OtherLogger()) // other_log: 2
}
myAwesomeContextFunction(1) // err: no context
myAwesomeContextFunction(2, OtherLogger()) // other_log: 2 |
This is a good example of the problems that library users will face when accessibility by receiver requires marking by the library author. |
I wonder how confusing, or complicated, would be to overload the
|
Can someone shed some light on §E.2 for me.
It looks like |
Some more answers to the comments below. But before, a general comment: there are definitely cases which are difficult or impossible to express in this design. In the process leading to it we've tried to ponder the likeliness of every scenario; things which we don't think are going to be done, or we think should be done in other way, get less priority.
You're actually correct, and for some time we discussed a similar design with two different keywords. However, at some point we realized that adding context parameters to a function is already a marker for "opting in to the feature", so
One important design decision was that we did not want to interleave context and value parameters. That complicates resolution too much; since value parameters have additional powers like default values or This gave us two options: put all the parameters at the front or at the end. From those, the front seemed the best place because some parameters are receivers, and receivers are conventially written at the beginning in Kotlin. Finally, the requirement to write the context before the Other potential syntactic options are explored in the previous iteration of context receivers.
That is the crux of the issue. After consideration, we as a team considered that the author of the API has primacy in this case over the user, since it designs the API as a whole, but also any documentation or educational material which explains the intended mode of use. We found no compelling reasons, in most cases, to override the library author design; hence the "escape hatches" are not particularly simple.
I honestly think the two options here are either have receivers which populate in some capacity the implicit scope, or just get rid of them completely and have only named context parameters (this is what Scala does, for example). |
In some sense, fun <A, R> with(x: A, block: A.() -> R): R
fun <A, R> context(x: A, block: context(A) () -> R): R
The problem is that |
This is not a bad thing! Keeping with the example of a class ConsoleLogger {
context fun log(message: String)
var outputStream: OutputStream
}
fun main() {
// when we create the logger, we use "explicit" syntax
val logger = ConsoleLogger()
logger.outputStream = STDERR
// and now we use this built logger contextually
context(logger) {
// outputStream is not accessible
log("Hello")
}
} |
context(MyScope) fun doSomethingInScope(block: context(MyScope) suspend Container.()->Unit){} It looks cumbersome. I guess it is not that bad since functions like this will exist only in libraries. One of the primary problems with previous prototype was ambiguity caused by mixing context and dispatch receivers in lambdas. It seems like introduction of While dispatch receiver could be (or could not) important for inheritance, it does not make sense for lambdas. I can't see difference between dispatch and context receivers for lambdas and function types. It is not that bad as it is right now, but I think it would be better to revisit one of the initial suggestions: |
An alternative name for One unaddressed thing is perhaps some way to bring in contexts without nesting. It would need language level support, but it would allow for more complicated features later on like bringing in a context inside a class, or bringing in a context perhaps in a file, or even for a whole module eventually (similar to how typeclasses work).
But perhaps this should be a wider proposal for kotlin to support some syntax to define a lambda to the end of the current scope. |
DSLs (§C) make heavy use of context receivers and if you want to extend an existing DSL you need more than one receiver. |
Do you plan a grace period, where we can use unmarked callables from a context, perhaps with some opt-in? |
Indeed! There are many possibilities, and from those we think
No, the idea is to enforce this rule since the beginning. Any feature we provide without restriction is difficult to restrict after the fact, since we would break some code. However, we'll work closely with the rest of the ecosystem to provide these annotations as soon as possible. We have already investigated which are the libraries more apt for this conversion, and talked informally to some of the maintainers. |
Thanks for the update, it's great to see how this is moving. First, as multiple people have mentioned before, I'm not a fan of the After thinking about it for some time, I think the contextual visibility is a good idea. One of the worries with context receivers would be that they would be abused in application code (in the same way that regular receivers are sometimes abused, but worse since there are fewer restrictions). However, since this visibility does not apply to regular receivers, I'm worried this may make the language harder to learn. However, I'm not convinced by marking functions with this visibility. As the KEEP explains (emphasis mine):
Also, as @serras said:
In all of these sentences, "it makes sense to use something in a contextual manner" is said about a type, not about an operation. I believe the ability to use something as an unnamed context receiver is a property of the type itself. context interface HtmlScope {
fun body(block: context(HtmlScope) () -> Unit)
} In this interpretation, the parameter-less version of
This is also a much simpler rule to explain to users, when inevitably someone will add a type that is not meant for it as a context receiver, and will not understand why IDEA autocompletes some methods but not others. As a library author, I'm having a hard time finding a use case in which some methods of a class would be allowed in contexts but not others—either the class is meant as a DSL, and all its methods are written with this use case in mind, or it's not, in which case none are. I believe having an explicit way to definitely mark which types were written with scope pollution in mind, and which were not, would be a great feature. (This would also potentially allow (If there is a chance that contextual classes are introduced later, I would prefer |
Will this be released for multiplatform as well straight away? |
That is the plan, full support in all platforms |
(1) Regarding the name declaration in
(2) Regarding §1.4 (definition of implicitness, i.e. how values for context arguments are resolved).
fun X.m() { ... }
context(_: X) fun n() {
m() // cannot resolve value of type X from context
}
context(_: X) fun m() { ... }
fun X.n() {
m() // ok, resolved the value of type X from the receiver scope
} Could you explain why this should be the case and what benefit it brings? I have not found convincing arguments in the proposal.
|
I prefer the syntax of the original request: fun (A, B).foo(value: String) {
...
} And i don’t understand why it’s necessary to name the contexts. |
@Irineu333 That may completely close off the potential path to tuples. val pairs: (Int, String) = (1, "a")
fun (Int, String).first(): Int = this.0 |
@Peanuuutz I don't think tuples will ever happen. They do not make a lot of sense with data classes and decomposition already there. As for best variant of KEEP-176, it was |
@Irineu333 Having multiple "true" receivers plays really badly with the language as it is currently. It makes many things ambiguous and creates risks of hard-to-understand code that changes behavior with new versions of libraries. Therefore, Context parameters are not true receivers. They are similar, but slightly different to avoid the main issues. Therefore they should have their own syntax. Now, whether they need a name or not is another question.
|
@CLOVIS-AI I do not agree. It was discussed a lot in #176. The only difference between dispatch receiver and context receiver is ability to inherit dispatch receiver. I the current proposal additional difference is that only dispatch receiver could be called as In lambdas, dispatch receivers do not make any sense as well as mixing between context receivers and dispatch receivers. The bracket variant is also applicable to named parameters: fun [context: Context, receiver: Receiver].doSomething( block: [Context, Receiver].() -> Unit ){...} I think it looks much better than: context(context: Context, receiver: Receiver)
fun doSomething( block: context(context: Context, receiver: Receiver) () -> Unit ){...} |
I don't think this: fun doSomething(block: (A, B).() -> Unit) {
...
} is harder to understand than this: fun doSomething(block: context(A, B) () -> Unit) {
...
} or this: fun doSomething(block: [A, B].() -> Unit) {
...
} Maybe the problem is dealing with line breaks. Or perhaps it's because, in the future, they want to add context for other things, like classes. |
It's not the only difference. Here is another: context(foo: Foo)
fun bar()
Foo().bar() // does not compile
Why? If two concepts are different, they should have different syntax. We don't have parameters that magically change how they work depending on the order in which they are placed. Using your syntax, but splitting context parameters and the receiver: protected suspend inline fun <T : List<String>> [context: Context, receiver: Receiver] T.doSomething( block: [Context, Receiver].() -> Unit ){...} Each time a new language feature is added, the harder it is to find the name of the function. We can't continue having everything on the same line, Kotlin is already starting to be quite hard to read. @Irineu333 It's not about syntax, it's about concepts. @serras it could be great to add a section to the KEEP summarizing why the team didn't go with multiple dispatch receivers, I see the question being asked quite often. |
@CLOVIS-AI I get it now! Thanks for the explanation. Considering the following syntax: context(A)
fun B.doSomething() We have the following behavior: with(B()) {
A().doSomething() // does not compile
}
with(A()) {
B().doSomething() // compiles
} Whereas, for the definition: fun (A, B).doSomething() The behavior changes to: with(B()) {
A().doSomething() // compiles
}
with(A()) {
B().doSomething() // compiles
} I understand the difference now. The syntax |
Mixing dispatch and context receivers is the major problem in all my experiments with previous iteration of context receivers. It should be prohibited wherever it is possible. If we replace fun T.function() or fun [input: T, output: R].function() They look similar. Dispatch receiver is also complex concept that would make adoption much harder. |
@SPC-code I have a question: what if I want to add additional contexts to an extension function? For example: context(A, B)
fun C.doSomething() How would you reproduce this using brackets? |
Maybe it's finally the time to introduce "receiver blocks"? extension <L> for L
where L: List<String>
in context: Context
{
public suspend inline fun doSomething(block: (this: L, in context: Context) -> Unit) {
// Proposed usage
this.block()
// Same as
block(this, context)
}
} Why blocks? Functions with context often come in a group. With current syntax, it's hard to both write and read multiple functions because of too much repetition. This is already the case for extension functions on generic collections. |
The idea is to avoid mixing context receivers and "regular" receivers at all. I've experimented a lot with context receivers (besides being one of the authors of KEEP 176) and in my experience, mixing them brings a lot of problems. Do you have a case, where it is needed? In my opinion, I do not strongly object to the current design since it won't be used a lot outside of libraries, but I like square brackets design much more because it is consistent. One must also remember that idea of using |
I see, so the equivalent of: context(A, B)
fun C.doSomething() would be: fun [A, B, this: C].doSomething() I think it could also be: fun [A, B, receiver: C].doSomething() |
Now I understand why this has been under discussion for years. |
I fill like the usage of context receivers is very similar to varargs. How it would look: fun myFun(with myContext1 : MyContext1, with myContext2 : MyContext2, otherParam1 : Param, otherParam2 : Param){
...
}
fun otherFun{
context(myContext1(),myContext2()){
myFun(Param(),Param())
}
} If only one context is used than a syntactich sugar might be added: fun myFun(with MyContext, otherParam1 : Param, otherParam2 : Param){
...
}
fun otherFun{
context(myContext()){
myFun(Param(),Param())
}
} |
The ide is cool, but the syntax is ugly full of unnecessary formalism and not kotlin like. I suggest: extend <L> L where L : List<String> {
context (myContext:Context){
public suspend inline fun doSomething(block: (this: L, in context: Context) -> Unit) {
// Proposed usage
this.block()
// Same as
block(this, context)
}
}
} This syntax allows defining multiple extentions for the same type in different contexts too. |
Yeah I agree splitting dispatch receiver and context receiver is better. extend <C> Collection<C> {
fun <D> maxBy(selector: (C) -> D): C? { /* ... */ }
fun <D> minBy(selector: (C) -> D): C? { /* ... */ }
}
extend <C> Collection<C> where C: Comparable<C> {
fun max(): C? { /* ... */ }
fun min(): C? { /* ... */ }
}
context (logger: Logger) {
fun run(tag: String, block: context(Logger) () -> Unit) {
logger.debug("BEGIN $tag")
block()
logger.debug("END $tag")
}
}
extend Server {
context (env: Env) {
suspend fun start(loop: context(Env) suspend () -> Unit) { /* ... */ }
fun stop() { /* ... */ }
}
}
context (builder: StringBuilder) {
extend String {
operator fun unaryPlus() {
builder.append(this)
}
}
} |
There is another difference: varargs appear on the callsite, context parameters do not. It is important for the language (and has already been discussed both in this thread and the previous KEEPs and issues) that the parameter list when a function is declared, and the argument list when a function is called, to be similar. Especially because by default, the parameter order is important (parameters are by default resolved by index). Changing this would make functions harder to read. The reason this KEEP isn't progressing isn't because of syntax. It's because the behavior in quite a few edge cases is weird and hard to understand. Once these cases are handled, the proposal will (hopefully) progress enough that it at least becomes plausible that this feature reaches Kotlin, no matter its shape. |
I see this thread is picking up steam again. Let me reiterate that context parameters are going to be implemented as the current proposal, and afterwards we'll make a retrospective and decide whether we need to make any adjustments for the final implementation. In the meantime, please don't hesitate to ask for further clarifications. I would kindly ask everybody not to bring questions that were already answered in this thread or in the Q&A about decisions in the KEEP proposal itself. In particular,
|
Ok then, why do you mention "... dropping receiver ..." now? I have the feeling that a syntax without name of the form I see and understand that the discussion is already quite convoluted and my intention is not to add more to it. But I would propose one thing after the implementation: Perform a prototypical migration of the kotlinx.coroutines library, not paying attention to backward compatibility, as if context parameters would have been available from the start. Just to see how the concept would fit to the demands in this library. I'm curious on how often "_:" would appear there in relation to all "context" usages. |
I have just reread the current version of the document I previously read one year ago. I would like to thank @serras as I am far more satisfied with its current state and chosen design and its simplicity. However, I have a couple of comments about the design. Getter/setter contextsI suppose it makes sense to allow getters/setters to have additional context parameters. The general reason is that for modifying an object, you may be practical to require a wider context than just for reading, e.g. Syntactically speaking, I would expect these two code snippets to be equivalent: context(A)
val x: T
get()
set(value) val x: T
context(A)
get()
context(A)
set(value) This will allow users to do the following: val x: T
context(A)
get()
context(A, B)
set(value) or val x: T
context(A)
get()
context(B)
set(value) Of course, these features can be added later. Placing generics before contexts
I see two results:
|
Question: is it bad if I use context receivers as a scope to deprecate/introduce compiler errors? e.g. fun A.haveFun() // This is ok
@Deprecated("No fun at work", level = DeprecationLevel.ERROR)
context(_: WorkScope)
fun A.haveFun(): Nothing // This is not ok Where having Basically my use case is that I have some extension functions that are slow, and they end up being precomputed in the scope, so it's a mistake to use the slow functions when the the code can access the precomputed data when the scope is present. Both functions are in the same package so they're always imported together. |
It is definitely not the preferred option. Note that this would only work with one function having no context parameters and the other having some, you cannot use this to distinguish between different variations. |
In the current version of context receivers, I've come up with a use case of creating a helper function to wrap commonly used contexts (especially in tests).
AFAIK it's not easily possible to call myFun with the correct contexts. Will this be solved in the new proposal? Also, will there be an option to create alias for commonly used set of contexts? Or is it up to the user to set up an interface with the contexts as members? |
Now that 2.1.0 is released, can we check in about the migration plan/timeline? KT-67119 hasn't been updated for a while, and it is marked as "fixed" which prevents subscribing to it. |
This is an issue to discuss context parameters. The full text of the proposal can be found here.
The text was updated successfully, but these errors were encountered: