Toying with Kotlin's Context Receivers(blog.frankel.ch)
blog.frankel.ch
Toying with Kotlin's Context Receivers
https://blog.frankel.ch/kotlin-context-receivers/
10 comments
First thought when I saw this feature was "no please not another Scala-ish" feature. Don't like "with()", don't like scope function naming (always have to look them up), don't like functions with expression bodies (without curly braces).
These features encourage chasing after diminishing marginal return in aesthetics and conciseness, taking up developer time that would be spent better elsewhere. They are also needlessly complicating the language and IMO don't carry their weight.
These features encourage chasing after diminishing marginal return in aesthetics and conciseness, taking up developer time that would be spent better elsewhere. They are also needlessly complicating the language and IMO don't carry their weight.
I have used Kotlin in production for a while, and I like it for its immutable by default stance. I also like that Kotlin makes it at least appear to allow functions as first class constructs (outside of a class). Its type system is very nice as well.
However, I do feel like Kotlin has too many features as a language.
What I'd really like to see is a typed functional language with a minimal feature set, and that's how I use Kotlin. That said, using a language subset within a lang that has a large surface area tends to fall down over time. Team discipline can only go so far.
However, I do feel like Kotlin has too many features as a language.
What I'd really like to see is a typed functional language with a minimal feature set, and that's how I use Kotlin. That said, using a language subset within a lang that has a large surface area tends to fall down over time. Team discipline can only go so far.
The C++ conundrum. Restraint and discipline is a rare trait among language designers it seems.
I'm really not a fan of implicit context being passed around like this. I hated Scala's implicit feature although I'm not familiar with the recent apparent improvements referenced in the article.
In a larger 'with' block, you cannot tell which invocations will interact with or change the state of a context object without looking at site of the definition of the function.
For me, when reading others' code, being able to construct a mental model of what the effects of a line of code in isolation is a vital property for code to be readable/comprehensible. Without implicits, you can mentally constrain what effects a line like "x.y(z)" will have on the state of the program.
One issue I had with the Scala feature was how it made refactoring is more fraught - as it's not clear which pieces of code in the 'with' block can be moved elsewhere from just reading the code.
Context objects behave like a form of global variable except their use can be even more confusing as Foo@this will refer to different things depending on the runtime call stack. And similarly, it bakes in a singleton aspect to the context objects. The example model from the article should allow functions to operate on multiple Transaction or AccountRepo instances, for example.
In a larger 'with' block, you cannot tell which invocations will interact with or change the state of a context object without looking at site of the definition of the function.
For me, when reading others' code, being able to construct a mental model of what the effects of a line of code in isolation is a vital property for code to be readable/comprehensible. Without implicits, you can mentally constrain what effects a line like "x.y(z)" will have on the state of the program.
One issue I had with the Scala feature was how it made refactoring is more fraught - as it's not clear which pieces of code in the 'with' block can be moved elsewhere from just reading the code.
Context objects behave like a form of global variable except their use can be even more confusing as Foo@this will refer to different things depending on the runtime call stack. And similarly, it bakes in a singleton aspect to the context objects. The example model from the article should allow functions to operate on multiple Transaction or AccountRepo instances, for example.
Consider, (in my own syntax),
By being explicit about which "nonlocal"s are required for an API, we can control this better. And also compose-in other APIs, etc., add toJson() at the end.
(NB. nonlocal in the sense that they are common to most/all uses of an API)
Python does this with decorators, but if it would add a little function chaining/compositon syntax, it would be more powerful.
on(table)
select(blah)
where(blah)
groupby(blah)
this is basically just, table.select(blah).where(blah).groupby(blah)
but this becomes inflexible when we want `table` to track more than just itself: table.connect().select(blah).where(blah).groupby(blah)
suddenly, `table` holds fields which function like your "global variables". instead, on(table, connection)
select()
...
In otherwords OO already has this implicit mechanism at work in a highly action-at-a-distance manner, with unweildy objects functioning like a box-of-globals.By being explicit about which "nonlocal"s are required for an API, we can control this better. And also compose-in other APIs, etc., add toJson() at the end.
(NB. nonlocal in the sense that they are common to most/all uses of an API)
Python does this with decorators, but if it would add a little function chaining/compositon syntax, it would be more powerful.
Yes, you can achieve also reduce local readability by passing objects containing buckets of state around. But such classes always smell to me - particularly if component instances have other aliases. But I try to avoid it by designing distinct and hopefully coherent service or context classes. And this Kotlin feature effectively requires you structure your context/service/etc. classes in this way - the point is to allow multiple implicit contexts.
When you say 'action-at-a-distance' - I would argue that the "distance" is not on the same scale. One crosses statements invisibly while your example, in all languages I've used, would still constitute a single statement.
But fundamentally, this is an argument about style as it's not a pure binary distinction, so I don't think there's a definitive right or wrong here.
Subjectively - in my experience, codebases (remembering one in Scala in particular) which rely heavily on implicit context objects are more difficult to understand. For me, when trying to get my head around an unfamiliar codebase, explicit is better than implicit unless it introduces so much verbiage that the intent is hidden.
When you say 'action-at-a-distance' - I would argue that the "distance" is not on the same scale. One crosses statements invisibly while your example, in all languages I've used, would still constitute a single statement.
But fundamentally, this is an argument about style as it's not a pure binary distinction, so I don't think there's a definitive right or wrong here.
Subjectively - in my experience, codebases (remembering one in Scala in particular) which rely heavily on implicit context objects are more difficult to understand. For me, when trying to get my head around an unfamiliar codebase, explicit is better than implicit unless it introduces so much verbiage that the intent is hidden.
I read this as a means of making the implictness of OO, explicit. Ie., it is syntax for specifying what `this` is
Inheritance, fluid changing + properties, etc. are all implicit in exactly the same way. OO programmers have just got used to them
Inheritance, fluid changing + properties, etc. are all implicit in exactly the same way. OO programmers have just got used to them
Yup, I'm skeptical too. I hope JetBrains support this feature really well in their IDE, that will hopefully make code using context receivers a bit easier to read. But of course Kotlin code should not only be readable in Android Studio...
> But of course Kotlin code should not only be readable in Android Studio...
Intellij Idea Community Version reads it just fine ;)
Intellij Idea Community Version reads it just fine ;)
Context receivers are also being considered as a poor man's DI/insane man's DI, depending on how you see it. Probably both. After all, if you have a class that requires A, B and C to do its work, marking it as needing those three in context make it super easy to just always have to provide them.
Unfortunately (fortunately? Because I know the evil, disgusting things I would do if it was easier), right now you have to nest with(x) or x.apply { } blocks to have everything in scope. A with(a, b, c) { } syntax would be very welcome, but I don't think functions of type (A, B, C).() -> Unit are going to exist any time soon.
Unfortunately (fortunately? Because I know the evil, disgusting things I would do if it was easier), right now you have to nest with(x) or x.apply { } blocks to have everything in scope. A with(a, b, c) { } syntax would be very welcome, but I don't think functions of type (A, B, C).() -> Unit are going to exist any time soon.
+1 for "insane".
I'm honestly baffled by all this context nonsense in Scala, React and now Kotlin. Twenty years ago, Spring (among others) showed us how to do proper wiring of separate elements in a clean way. Everything was simple and crystal clear (if a bit verbose). Programming became mostly additive (rather than multiplicative) in complexity and programs could easily be broken down to pieces easy to understand and implement.
And now we are back pass parameters to functions off-band like in the times of VB6? Seriously!?
Get off my lawn...
I'm honestly baffled by all this context nonsense in Scala, React and now Kotlin. Twenty years ago, Spring (among others) showed us how to do proper wiring of separate elements in a clean way. Everything was simple and crystal clear (if a bit verbose). Programming became mostly additive (rather than multiplicative) in complexity and programs could easily be broken down to pieces easy to understand and implement.
And now we are back pass parameters to functions off-band like in the times of VB6? Seriously!?
Get off my lawn...
Off-band is a bit of a misnomer. The cases where you end up with enough context to actually be able to call these functions are either "oh god what the fuck are you doing" or "you nested with/applys on purpose". It doesn't magically conjure parameters out of thin air. It's an extension of receiver functions, where, if before you had a function that took A, B and C as dependencies, the best you could write was A.doThing(b: B, c: C, paramIActuallyNeed: Int), or wrap everything in a class (which goes against what Kotlin is trying to do).
In addition, there are some actual interesting things to do with it! It's ultimately syntax sugar to hide external dependencies. If tomorrow all your calls to Int.add(other) need to all be logged to database, I'd rather add context(DatabaseWriter) and have all the benefits of it (including compilation errors), rather than polluting my function signature. It's not like Context providers in React or in Compose, where you just call and assume it's in scope and it crashes at runtime if it's not: context providers will not let you compile unless it is certain they are in scope.
Traditional dependency injection is still very much done and the standard in Kotlin, except without the pile of crap that is declaring beans in XML
In addition, there are some actual interesting things to do with it! It's ultimately syntax sugar to hide external dependencies. If tomorrow all your calls to Int.add(other) need to all be logged to database, I'd rather add context(DatabaseWriter) and have all the benefits of it (including compilation errors), rather than polluting my function signature. It's not like Context providers in React or in Compose, where you just call and assume it's in scope and it crashes at runtime if it's not: context providers will not let you compile unless it is certain they are in scope.
Traditional dependency injection is still very much done and the standard in Kotlin, except without the pile of crap that is declaring beans in XML
I'm keeping an eye out on Kotlin's context receiver change, as I would much prefer a way to tackle dependency injection without using annotations and definitely without using XML. DI frameworks are already the insane man's DI.
Have you considered something like Dagger + Anvil (https://github.com/square/anvil) ? It works well enough that it gets out of your way. Is it doing insane things in the background at compilation time ? Absolutely. But it's worth it. And to save time in debug, you can shim in dagger-reflect to avoid the cost of having to run annotation processors.
I would say the state-of-the-art today is Micronaut's DI [1].
[1] https://docs.micronaut.io/1.0.0/guide/index.html#ioc
[1] https://docs.micronaut.io/1.0.0/guide/index.html#ioc
Uh, that's a service locator with an annotation processor to discover injectable targets. It's far from being state of the art. It's like Koin, except slightly less manual. A far cry from automatically discovered injection
> A far cry from automatically discovered injection
Ok, then what's the wonderful DI system that has that?
Micronaut was created by ex-Spring developers who had experience with all the earlier DI systems, including Dagger, Guice, PicoContainer etc.
I would love to learn what people have come up with that's even nicer to use!
Ok, then what's the wonderful DI system that has that?
Micronaut was created by ex-Spring developers who had experience with all the earlier DI systems, including Dagger, Guice, PicoContainer etc.
I would love to learn what people have come up with that's even nicer to use!
IMO the state of the art is Weld. With CDI lite you now can have compile time DI instead of having it resolved at runtime.
https://weld.cdi-spec.org/
https://weld.cdi-spec.org/
Micronaut DI is compile-time for almost everything.
It seems really confusing that the context object becomes an implicit `this`. It is already hard enough to understand what scope a name will be looked up in. Many languages suffer from this when they have a kind of `with` block. Ironically, VB6 gets it right. There you have to qualify members with a leading dot, if they are to be looked up in the context, e.g.:
' Haven't written this in 20 years so maybe it is not 100% correct :-)
With myForm
.Title = "Hello World"
.Maximize()
End With
When I heard "context receivers", I thought something completely different and got excited: Sometimes you need to pass a parameter to a function that only gets called several levels deep. It is ugly to add that parameter to every intermediate function. But it is even worse to add a global variable. The OOP solution would be to put all the functions as methods on an object, and pass the data via a field on the object. But what if you could pass the parameter like this (pseudocode): function outer() {
use-extra-context(logcolor="RED") {
foo();
}
}
function foo() { bar(); }
function bar() { baz(); }
function inner() takes-extra-context(logcolor) {
print("Something", color=logcolor);
}
Basically something like dynamical scope, or thread local storage. (Or reading from the other comments, maybe this proposal does just that and I missed something?)Seems the examples are contrived.
* Transactions are first-class in most RDMBS, performing I/O in between each one, and then sending a commit or rollback seems bizarre when you can send off one, not to mention implementing it in the host language is unnecessary.
* It seems to be using blocking I/O when most platforms have evolved to concurrent I/O.
* If the host language does indeed expose high level start/commit/rollback, it should be exposed minimally with a RAII-safe callback routine, something like `runTransaction`. You wouldn't want to see the former in the wild.
* It seems to argue for the use of context / implicit parameters, where I think you really don't want to use it in this case. Implicits or Context seems better for something like executors or React Context.
* Are they really using floats for monetary value?
* Transactions are first-class in most RDMBS, performing I/O in between each one, and then sending a commit or rollback seems bizarre when you can send off one, not to mention implementing it in the host language is unnecessary.
* It seems to be using blocking I/O when most platforms have evolved to concurrent I/O.
* If the host language does indeed expose high level start/commit/rollback, it should be exposed minimally with a RAII-safe callback routine, something like `runTransaction`. You wouldn't want to see the former in the wild.
* It seems to argue for the use of context / implicit parameters, where I think you really don't want to use it in this case. Implicits or Context seems better for something like executors or React Context.
* Are they really using floats for monetary value?
A lot of these sorts of language features feel to me like workarounds for a lack of methods with multiple dispatch: methods like the transfer method seem to belong to a collection of classes but typical OO languages force you to pick a single class to attach them to.
Nothing's stopping you from making a Transferable interface. Everything that implements it has to implement transfer(), and you can then make an extension function on Transferable instead of your implementation. Additionally, multiple dispatch in JVM languages (except Groovy because it implements multiple dispatch already) can just be done by having two layers of classes. Yes, it's a holdover from the JVM being heavily class based, but nothing preventing you from doing that. Hell, define both your classes, and put all your logic in a single file with extension functions. Kotlin is quite free form when it comes to where you should put your things, what it should be attached to, etc.
Clojure also has it via its simplified CLOS implementation.
There is a difference between multidisptach (all/some arguments decide what is being applied) or contextual dispatch, where dynamic scope decides that.
Useful for things like React Context when using Kotlin/JS.
The other way to look at this is this lets you model your functions dependencies into two different groups: 1/ "Data" your functions is working with - passed as good ol' arguments 2/ "Services" your function needs to do certain operations - passed as context
I guess it's slightly nicer when thought that way, though good luck getting a team of 200 developers on the same page regarding how to use these advanced features.
Code review:
The other way to look at this is this lets you model your functions dependencies into two different groups: 1/ "Data" your functions is working with - passed as good ol' arguments 2/ "Services" your function needs to do certain operations - passed as context
I guess it's slightly nicer when thought that way, though good luck getting a team of 200 developers on the same page regarding how to use these advanced features.
Code review:
A: "Why are you passing that data as context?"
B: "makes it cleaner"
Cleanliness lies in the eyes of beholder, so how do you argue with that?In my first programming job I was working on a financial application in VB6 where there were two global variables that were used to pass extra parameters. If there was something to pass which was not on the signature you would just leave it on the first variable. Sometimes that variable was already used to pass something to the current function so you would use the second one. I kid you not.
Why all this nonsense? No idea. When asking people looked at me as if I was some kind of purist. They didn't seem to even understand this was not a good idea.
Now I find myself 23 years later, after having long left all that imperative crap, getting TDD, Spring and all those niceties just to find people to come back with dynamic variables as if they were a good idea instead of a needed hack when you don't have something better. I guess next trend will be factories and singletons. I just don't get it. It's very sad.
Why all this nonsense? No idea. When asking people looked at me as if I was some kind of purist. They didn't seem to even understand this was not a good idea.
Now I find myself 23 years later, after having long left all that imperative crap, getting TDD, Spring and all those niceties just to find people to come back with dynamic variables as if they were a good idea instead of a needed hack when you don't have something better. I guess next trend will be factories and singletons. I just don't get it. It's very sad.
> so how do you argue with that?
Quite simply: "cleanliness" might be nice, but readability and maintainability are more important. The former is purely subjective, the latter are actually quite well defined.
Quite simply: "cleanliness" might be nice, but readability and maintainability are more important. The former is purely subjective, the latter are actually quite well defined.
> readability and maintainability are ... actually quite well defined.
Would you care to expand on this? My experience has been that readability and maintainability are very subjective outside of extreme cases.
Would you care to expand on this? My experience has been that readability and maintainability are very subjective outside of extreme cases.
"Readability" concept, as commonly understood, is a misnomer - when you say something is readable, you most often mean "it's close to my expectations of how it should be written," regardless of how easy or hard it would be to read. This meaning is very subjective, as there are numerous ways of writing the same thing, and which one you would choose depends on everything you've read and written to date. It's pretty trivial to prove how subjective the notion is: find a few lines of code you think are very readable right now and keep them somewhere, then try to read them a year later. New experiences you've had during the year will, with high probability, change the way you'd write that code, making it less (a bit or more, depending on what you experienced) "readable" to you.
You can, however, cast readability as a problem in the domain of cognitive science - how easy or hard it is to understand the ideas based on their textual descriptions.
We more-or-less know how human memory works: there's a working area, short-term memory, long-term memory, and the need to copy data between these segments. The working area is limited in capacity, short-term memory is volatile, and long-term memory is costly to access. In addition, we know that context switches are expensive, and if they last long enough, they can break the flow - or rather, make your working memory reset itself. You then need to reassemble all the pieces required for solving the problem again, from scratch.
Readability is a score that says how many operations your brain must perform before fully converting the description into understanding. The more readable description requires holding fewer pieces/concepts/ideas in your working memory, minimizes the need for context switches, and doesn't rely on long-term memory too much.
I felt this would be the case for a long while, but I only recently read a book where most of my suspicions were proved correct. The title is: "The Programmer's Brain - what every programmer should know about cognition," and I can't recommend it enough. It's a bit boring at times, but it provides a good explanation and practical guidelines for writing code that actively supports your cognitive process - or, in other words, is readable.
One example: to abbreviate or not? Abbreviation may look more readable because it doesn't obscure other nearby identifiers. On the other hand, each time you see an abbreviation, your brain has to expand it, adding to its workload. In this light, you might be tempted to say that we should never abbreviate identifiers. That is not so: other than the brain, between the code and its understanding sits an eye. AbstractFactoryOfConreteFactoryWorkerBean-style identifiers are just as bad as abbreviations, because then your eye has more work to do. So, keep your identifiers not abbreviated as long as you can see the whole identifier in a single glace - start abbreviating after that. Ideally, keep identifiers between 1 and 5 full words.
The book discusses a lot of similar characteristics of source code, presents empirical studies that back up many of its claims, and in general gives you quite a lot of insight into the mechanics of reading (and writing, but less so) programming languages.
You can, however, cast readability as a problem in the domain of cognitive science - how easy or hard it is to understand the ideas based on their textual descriptions.
We more-or-less know how human memory works: there's a working area, short-term memory, long-term memory, and the need to copy data between these segments. The working area is limited in capacity, short-term memory is volatile, and long-term memory is costly to access. In addition, we know that context switches are expensive, and if they last long enough, they can break the flow - or rather, make your working memory reset itself. You then need to reassemble all the pieces required for solving the problem again, from scratch.
Readability is a score that says how many operations your brain must perform before fully converting the description into understanding. The more readable description requires holding fewer pieces/concepts/ideas in your working memory, minimizes the need for context switches, and doesn't rely on long-term memory too much.
I felt this would be the case for a long while, but I only recently read a book where most of my suspicions were proved correct. The title is: "The Programmer's Brain - what every programmer should know about cognition," and I can't recommend it enough. It's a bit boring at times, but it provides a good explanation and practical guidelines for writing code that actively supports your cognitive process - or, in other words, is readable.
One example: to abbreviate or not? Abbreviation may look more readable because it doesn't obscure other nearby identifiers. On the other hand, each time you see an abbreviation, your brain has to expand it, adding to its workload. In this light, you might be tempted to say that we should never abbreviate identifiers. That is not so: other than the brain, between the code and its understanding sits an eye. AbstractFactoryOfConreteFactoryWorkerBean-style identifiers are just as bad as abbreviations, because then your eye has more work to do. So, keep your identifiers not abbreviated as long as you can see the whole identifier in a single glace - start abbreviating after that. Ideally, keep identifiers between 1 and 5 full words.
The book discusses a lot of similar characteristics of source code, presents empirical studies that back up many of its claims, and in general gives you quite a lot of insight into the mechanics of reading (and writing, but less so) programming languages.
Can someone provide a decent, frequent example that merits adding all this overhead to a language? I'm having a very difficult time thinking why use this instead of plain interfaces and extension methods. Heck is just doing `Service.transfer(trx, args)` really such an abomination that needs correction?
Is this private class fields with a different syntax?
I'd appreciate a comparison with Scala 3 given/using or the previous implicits
Replacing a function call `foo(bar, baz)` with just `foo()` by bringing `bar` and `baz` into context is not meaningfully better in any way. I'd even argue it reduces the readability of the code due to implicit receivers.