Closed shadrina closed 3 months ago
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 void
return type, it should be Unit
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 context(T)
construct could be mixed with a function call in the code and it should be treated properly. But what Is more important, it confuses the reader. When I read the code, I can't see clearly that this declaration is attached to the following declaration. I can understand the reasoning behind not using @
(a lot of people have negative emotions about that), but some kind of visible syntax would be nice. Another concern is about using round brackets for types. In kotlin, we always use round brackets for instances, so context(MyContext)
reads as a companion object of MyContext
, not as the type itself. In Groovy one can use class name instead of class and it brings a lot of confusion sometimes. So I would recommend at least using triangle brackets <>
for receiver type to provide clear separation. Like context<MyContext> fun A.doB()
.
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 with(a,b){ doSomething}
, but in some cases, we also can have:
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 🙌
context
SyntaxI thought exactly the same as @altavir.
Here it's especially bad: var g: context(Context) Receiver.(Param) -> Unit
. Looks like a call within a function body.
context<A, B>
is easier to understand and also makes more sense to me.
A
and B
.context<A> fun …
isn't valid Kotlin code today (afaik) even with a line break.context<Comparator<T>>
still aren't an issue.Could also be context<A & B>
instead, having future union types in mind. May cause problems with this@A
syntax.
And then later on allows typealias C = A & B
with context<C>
.
The use case Creating JSONs with JSONObject and custom DSL is basically a builder DSL. However in Contexts and coding style you write "Context receivers shall not be used for such builders". I'd say it depends on the DSL's purpose whether or not a context makes more sense.
I think the proposal should go deeper on how context receivers would work with annotations and the suspend
modifier. It only says:
Also,
suspend
and@Composable
functions can be retrofitted to work as if they are declared withcontext(PropertiesScope)
modifier and so will pass a set of current context properties via their calls, ensuring interoperability of the corresponding mechanisms.
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:
context
on the consuming side.with
on the providing side.withContext
in kotlinx-coroutines
.I already found code that accidentally used with
instead of withContext
.
With the new context
soft keyword this will probably get more confusing.
But it may also be an issue with kotlinx-coroutines
because withContext
is a really generic name.
@altavir @fluidsonic - about the suggested <>
syntax, the problem is that in context<Comparable<T>>
, Comparable
is a literal type while T
is a generic type, so the semantic meaning of <>
gets ambiguous.
@edrd-f
@altavir @fluidsonic - about the suggested
<>
syntax, the problem is that incontext<Comparable<T>>
,Comparable
is a literal type whileT
is a generic type, so the semantic meaning of<>
gets ambiguous.
I'm not sure I understand you correctly.
It could also be context<Comparable<Int>>
. Then Int
is a literal type used as an argument to a generic parameter.
The same way context<Foo>
has Foo
as an argument to a generic parameter that is unnamed and brought into scope in the function (more or less).
We're entering the ambiguous realm anyway. Consider object Foo
.
context(Foo)
in the fun
position is quite different from context(Foo)
in regular code.
@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 typeOf<>
which takes the type parameter. And in any case, it is better than round brackets since there is no distinction from the function call and, as I already said, association with the companion.
I would prefer something even more distinct, But triangular brackets are better than round ones.
@edrd-f please consider that context<Comparable<T>>()
is already valid Kotlin syntax.
The main tradeoff we had to make with the proposed syntax is that in case when context is generic, then the use of the generic parameter happens before its declaration, e.g (from Use cases section):
context(Monoid<T>) // T is used fun <T> List<T>.sum(): T = ... // ^^^ T is declared
I might've missed something, but why not put the contexts between <T>
and the function's receiver/name? Noise/parsing issues?
fun <T> context(Monoid<T>) List<T>.sum(): T = ...
I might've missed something, but why not put the contexts between
<T>
and the function's receiver/name? Noise/parsing issues?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. Secondly, this aligns more with where annotations are put and this feature for me is closer to annotations.
to @mcpiroman's point, maybe the syntax can put the context in the next line, similar to the existing where
syntax:
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 sum
function simply not existing without all the extra context and thus having to go hunt for the definition, instead it would be treated like an extension function with missing parameters and simply show you a red underline (and tell you what context parameters are missing).
Big +1 for the Scope Properties and Contextual Classes proposals. Scope properties would make my compiler plugins quite a bit nicer (put IrPluginContext
in context and a few other things), and contextual classes is exactly what I need for a deep learning library (so that layers can only be defined inside a graph).
to @mcpiroman's point, maybe the syntax can put the context in the next line, similar to the existing
where
syntax: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.
This was considered and rejected with
This placement would be consistent with Kotiln's where clause, but it is not consistent with receivers being specified before the function name. Moreover, Kotlin has a syntactic tradition of matching declaration syntax and call-site syntax and a context on a call-site is established before the function is invoked.
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 in
is already an existing keyword):
context fun <T> in Monoid<T> List<T>.sum(): T = ...
With multiple receivers and suspend
:
context suspend fun <T> with/in Monoid<T>, Scope<T> List<T>.sum(): T = ...
In my opinion, it turns context
into a function modifier that reads very similarly to suspend
while still allowing the definition of generics before their usage. However, it introduces an additional keyword in the function definition, making it slightly more verbose (although the removal of the parenthesis might make the syntax less "noisy").
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).
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.
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 T
together with a Monoid<T>
", a different keyword could be used:
context fun <T> List<T>.sum(): T given Monoid<T> = ...
which would read as "returning a T
given a Monoid<T>
. Although, according to the proposal, given
means something different in Scala which might be a problem and given
could also be confused to mean an argument (return T
given an argument of type Monoid<T>
).
With multiple receivers and suspend
:
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 context
as a modifier akin to suspend
. However, maybe the "usage of generic before its definition" is not a problem worth solving.
Regardless, I just thought I'd provide my 2 cents on the syntax (as everyone enjoys doing whenever new proposals appear :stuck_out_tongue:) and I'll be happy to use whatever comes out in the end. :)
Hello,
I'm looking at the example for AutoCloseScope
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.
scope.close()
? I do not see that declared anywhere.AutoCloseScope.defer
? That is not used anywhere.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?
@fvasco In the section VM ABI and Java compatibility, the Kotlin code .... ... Kotlin does not allows
void
return type, it should beUnit
The example in the text is correct. Unit-returning Kotlin functions are compiled to void
functions on JVM. Unit
is carried over in functional types, though.
@altavir 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.
Thanks a lot for bringing up the context(Ctx)
vs context<Ctx>
discussion. In fact, it was one of our biggest design discussions earlier in the design process, but we've failed to include it into the resulting text. I've added the corresponding section to the document. Please find the detailed answer in the Parentheses vs angle brackets section.
@altavir ... It could be called as
with(a,b){ doSomething}
, but in some cases, we also can have: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.
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 (c
in your example).
@fluidsonic
context
Syntax
I've answered on parentheses vs angle brackets above to @altavir. See the new Parentheses vs angle brackets section for answers.
Builders The use case Creating JSONs with JSONObject and custom DSL is basically a builder DSL. However in Contexts and coding style you write "Context receivers shall not be used for such builders". I'd say it depends on the DSL's purpose whether or not a context makes more sense.
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),
The point about adding named arguments does not seem to be valid to me. You are introducing a new syntax anyway, something like context<name: Type>
is as possible as context(name: Type)
, both will introduce new syntax, but the learning curve will be smoother for the first one. If we are talking about not blocking future possibilities while experimenting, won't it be better to use initially proposed annotation-like syntax @with<A, B>
which does not require new entities? We can decide on replacement syntax later, when we better understand all the use cases.
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 MatrixAlgebra
, but you also want inversion operation which could be done in different ways on the same algebra in DecompositionOps
. Basically, you pass to different contexts - one for algebra and one for decomposition, but in some cases, matrix algebra has one dedicated way of inverting, or even better algebra inherits DecompositionOps
for the default inversion method. Then I want to pass a single context that will suit both type requirements.
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.
@BenWoodworth I might've missed something, but why not put the contexts between
<T>
and the function's receiver/name? Noise/parsing issues?
fun <T> context(Monoid<T>) List<T>.sum(): T = ...
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 context(Ctx)
modifier as a separate line and we could not find a way to make it look nice if it is written after the fun
. However, with the "before the fun
" modifier it all plays out nicely:
context(Monoid<T>) // additional modifier, like @Transactional or @Logged
fun <T> List<T>.sum(): T = ...
// ^^^^^^^^^^^^^^^^^ the actual call signature a reader should be primarily concerned with
@nunocastromartins 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 = ...
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.
@TheBestPessimist I'm looking at the example for
AutoCloseScope
- What is
scope.close()
? I do not see that declared anywhere.- What is the purpose of
AutoCloseScope.defer
? That is not used anywhere.
It is all declared and used inside AutoCloseScopeImpl
which is not shown for the conciseness of the example.
The way I imagine using this example is... Is my understanding correct? Do you think of a different alternative?
Yes. That is the way we envision it, too.
If context
was annotation-like (@context
), couldn't it be declared for, say, all functions in a file at once? 🤔
@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? Should a comma separated list enough?
context TimeSource, TransactionContext, LoggingContext
fun doSomeTopLevelOperation() { ... }
@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 @Composable
annotation is also a context definition). The only rule we need to introduce is not all that starts with @
is annotation and it is fine by me.
@fvasco
context TimeSource, TransactionContext, LoggingContext fun doSomeTopLevelOperation() { ... }
And what about lambdas? You need always remember lambdas. And they could have additional modifiers like inline or suspend.
@altavir sorry, I miss the point. Can you explain better?
@fvasco
fun doSomething(block: suspend inline context TimeSource, TransactionContext, LoggingContext (String)->Unit)
fun doSomething(block: context TimeSource, TransactionContext, LoggingContext suspend inline (String)->Unit)
I think the point is on readability.
Assuming Kotlin gets decorators, will it be a possibility to combine decorators and context? Like
context(Connection)
decorator fun transactional(block: Transaction.() -> Unit) {
...
}
Can you elaborate on the behavior of a nullable context receiver?
Does context(Logger?)
mean that the context can be unmatched (and will then map to null), or does it require Logger?
to be a valid (nullable) context?
The first would be more valueable, the second would be more consistent.
@quickstep24 Can you elaborate on the behavior of a nullable context receiver? Does
context(Logger?)
mean that the context can be unmatched (and will then map to null), or does it requireLogger?
to be a valid (nullable) context? The first would be more valueable, the second would be more consistent.
Just as with other nullable receivers. It will require Logger?
to be available in the context and you'll be able to call Logger?
extensions from the scope of the function (methods on the Logger
will not resolve).
class C {
operator fun String.unaryPlus() = ...
}
This way has lots of limitations such as I cannot make this method an extension one because it already has a receiver. Also, such a way (creating a class or interface and adding extension properties and methods inside it) to declare DSLs looks quite contr-intuitive for newcomers, unlike the suggested new one. That is why it would be great to hear about what is going to happen with current implementations. Would they be deprecated or what?
I think the keyword within
would make code more readable. Some examples taken from the proposal, comparing context
vs. within
:
It would also be a better match for with
. For example:
The declaration-site would match the use-site really closely.
The declaration-site would match the use-site really closely.
@edrd-f within
makes sense when using with
but there are other ways to bring loggingContext
(for example) into scope.
I understand the signature readability requirements (https://github.com/Kotlin/KEEP/issues/259#issuecomment-863826364) and the need for multiline formatting (https://github.com/Kotlin/KEEP/issues/259#issuecomment-863829057).
Still, I think syntactically it looks weird to require parentheses. Parentheses group code that otherwise would interact differently with the surroundings, like in an expression ((x + y) * z) != (x + y * z)
or a function call f(a, b) != f a, b // What is this even?
.
The arguments for potential future use cases aren't good arguments for parentheses:
- [...] named context receivers [...]
Can be done with angle brackets too: context<name: Ctx>
or with optional parentheses for readability context<(name: Ctx)>
.
- [...] "type parameter use before declaration" [...]
Can be done with angle brackets too: context<T, Monoid<T>>,
although I'm not sure if that's unambiguous. It should be if T
doesn't resolve to a type, then it must be a type parameter.
- [...] modifiers on context receivers [...]
Can be done with angle brackets too: context<inline Ctx>
, e.g. like Kotlin already has <reified T>
.
Furthermore, I would prefer no brackets or parentheses at all, so that the context
modifier and its parameters become much more like other modifiers, e.g. internal suspend inline
. And it becomes less cumbersome to read and especially write. For example:
internal context A B C D suspend fun E.f(g: G): H = TODO()
Yes, that's complex and hard to read, but it is what it is. Line breaks should be allowed too:
internal context A B C D
suspend fun E.f(g: G): H = TODO()
Or with longer names:
internal context
AVeryLongName
ButThisOneAlsoIsLong
CanTheyBecomeEvenLonger
DontThinkSo
suspend fun ExtensionReceiver.functionName(
greatArgument: GreatArgument
): HelloIAmTheReturnType = TODO()
(or the same with angle brackets) (or with optional parentheses, just for grouping/readability) (or with optional commas between the context parameters, if somehow required for disambiguation)
It's not very different from the multiline issues when extending/implementing many super types: that also needs formatting guidelines and it doesn't need parentheses. It does use commas, though.
And it's also similar to the problems when using many type parameters, e.g. class X<T1, T2, T3, T4, T5, T6, T7>
: what if the generic types have long names? How to format the code?
I'd like to hear your opinion and professional insights on this!
Given that putting values that are used as primary receiver/arguments of function call in context is considered a bad style (and I agree with that point), have you considered restricting types that can be used as contexts? Like right now Result<T>
is somehow restricted to be returned from functions? That would limit some usage (like context(View)
on Android I'd guess).
@LDVSOFT
@ContextReceiver
interface SomeContext { }
context(SomeContext)
fun foo() { }
? Interesting, though I suspect I'd find that rather frustrating and limiting than helpful.
@mcpiroman Maybe something like that, I don't know. I liked the part there extension
interfaces are special things, but I guess between having two interface
-like entities that are different and just allowing things to happen second would be more kotlinish, but I'd like to hear Kotlin Team opinion on that.
@edrd-f
within
makes sense when usingwith
but there are other ways to bringloggingContext
(for example) into scope.
@raniejade yes, but it's always into scope, as you said. So within
still makes sense.
About "Scope properties" and "Contextual classes and contextual constructors", I want to show an example of how we can bring scope into a class using existing mechanisms (this code compiles):
interface HelloService {
fun execute(name: String)
}
interface Logger {
fun log(message: String)
}
class Context(val logger: Logger)
fun Context.HelloService() = object : HelloService {
override fun execute(name: String) {
logger.log("Hello, $name!")
}
}
val defaultLogger = object : Logger {
override fun log(message: String) = println(message)
}
fun main() = with(Context(logger = defaultLogger)) {
HelloService().execute(name = "Kotlin")
}
The difference here is that it's necessary to have a "god context" and this doesn't scale well in large applications. With multiple receivers, it will be possible to declare specific contexts:
within(LoggingContext, TransactionScope)
fun HelloService() = object : HelloService {
override fun execute(name: String) {
doSomethingWithTransaction(name)
log(name)
}
// ... other members ...
}
fun main() = with(LoggingContext(defaultLogger), TransactionScope()) {
HelloService().execute(name = "Kotlin")
}
What is nice about it is that it's not necessary to learn about new scoping mechanisms. Also, this "constructor function" is a pattern already used in some popular projects like Ktor.
The downside is having to create interfaces for every object and declaring function signatures twice.
Idea: default values for context
interface TransactionContext { ... }
val defaultTransactionCtx = object : TransactionContext { ... }
context(TransactionContext = defaultTransactionCtx)
fun foo() { ... }
fun main() {
foo()
val specializedCtx = object : TransactionContext { ... }
with(specializedCtx) {
foo()
}
}
Analogous to parameters, which the contexts really are.
This, if accepted, makes one more point behind using parentheses - to resemble syntax for parameters. While type parameters in future might have defaults too, these will be types (class Foo<T = String>
) whereas here they are values, so having context<Foo = bar>
would be not-so-nice.
I'd like to propose another idea, which may play quite well with the current syntax, let's take the json example
fun json(build: JSONObject.() -> Unit) = JSONObject().apply { build() }
typealias JsonBuilder = context(JSONObject, String) // `context` or whatever other syntax
infix fun JsonBuilder.by(build: JSONObject.() -> Unit) = put(this, JSONObject().build())
infix fun JsonBuilder.by(value: Any) = put(this, value)
fun main() {
val json = json {
"name" by "Kotlin"
"age" by 10
"creator" by {
"name" by "JetBrains"
"age" by "21"
}
}
}
This expands the usage of typealias
, which actually makes perfectly sense also in this scenario, imho
@mcpiroman
Idea: default values for context
This opens the path for global contexts, increasing the potential for abuse of this feature.
For Named context receivers, I believe there's a simple (albeit slightly verbose) solution. I believe the following should (hopefully) work:
// In stdlib
context(T)
inline fun <T> fromContext(): T = this@T // Could absolutely be named better, but this naming is good enough for this example
// In user code
context(TimeSource, TransactionContext, LoggingContext) // I know it is considered a BAD and unidiomatic example but it's just here for the sake of showcasing how this can work
fun doSomeTopLevelOperation() {
val myTimeSource = fromContext<TimeSource>()
val logger: LoggingContext = fromContext() // Type inference should be able to easily figure this one out
doSomethingMagicalWithTransaction(fromContext()).map { logger.log(it) } // Type inference should realise that the type parameter needed here is TransactionContext and so the fromContext call should work properly.
}
fun doSomethingMagicalWithTransaction(transaction: TransactionContext): Result<MyDesiredType> = Result.success(MyDesiredType("yay!"))
With some tweaking and workarounds, the above example can be rewritten to work today which functions pretty well, ableit with some slight issues due to the contexts being in different resolution hierarchy levels
@edrd-f
This opens the path for global contexts, increasing the potential for abuse of this feature.
I do not see how global e.g. file-level contexts are bad. On the contrary, I see a lot of uses for them. As long as they are explicit, it seems to be OK.
Idea: default values for context
can't this be done by simply declaring an overload that doesn't take the contextual parameter and then passes the default value to the overload that needs the contextual parameter? I feel as though maybe default contexts could be quite rare in practice, and so having a special syntax for them is kind of unusual. Alternatively, we could also have the context be context(TransactionContext?)
, have a with null
in each caller, and then handle the null with something along the lines of with this@TransactionContext ?: defaultTransactionContext
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.