Toying with Kotlin's Context Receivers
blog.frankel.ch
blog.frankel.ch
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.
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.
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.
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.
Intellij Idea Community Version reads it just fine ;)
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.
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.
Inheritance, fluid changing + properties, etc. are all implicit in exactly the same way. OO programmers have just got used to 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.
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!
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...
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
' 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?)* 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?
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?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.
Would you care to expand on this? My experience has been that readability and maintainability are very subjective outside of extreme cases.
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.
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.