6 ms·
Toying with Kotlin's Context Receivers
- fiddlerwoaroof 4y agoA 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.
- ohgodplsno 4y agoNothing'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.
- pjmlp 4y agoClojure also has it via its simplified CLOS implementation.
- artemonster 4y agoThere is a difference between multidisptach (all/some arguments decide what is being applied) or contextual dispatch, where dynamic scope decides that.
- ohgodplsno 4y agoContext 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.
- yen223 4y agoI'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.
- ohgodplsno 4y agoHave you considered something like Dagger + Anvil (https://github.com/square/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.
- brabel 4y agoI 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 https://docs.micronaut.io/1.0.0/guide/index.html#ioc
- ohgodplsno 4y agoUh, 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
- brabel 4y ago> 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!
- vips7L 4y ago
- derriz 4y agoI'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.
- rob74 4y agoYup, 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...
- scns 4y ago> But of course Kotlin code should not only be readable in Android Studio... Intellij Idea Community Version reads it just fine ;)
- mjburgess 4y agoConsider, (in my own syntax), 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.
- haroldadmin 4y agoWhile I really like Kotlin in general, I'm not a fan of this feature. It feels like unnecessary syntax sugar for functions that accept multiple parameters. 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.
- MrBuddyCasino 4y agoFirst 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.
- gorjusborg 4y agoI 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.
- MrBuddyCasino 4y agoThe C++ conundrum. Restraint and discipline is a rare trait among language designers it seems.
- jatins 4y agoUseful 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: 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?
- klibertp 4y ago> 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.
- grncdr 4y ago> 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.
- klibertp 4y ago"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.
- SemanticStrengh 4y agoI'd appreciate a comparison with Scala 3 given/using or the previous implicits
- captainmuon 4y agoIt 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?)
- whoisthemachine 4y agoIs this private class fields with a different syntax?
- throwaway234322 4y agoSeems 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?
- Daishiman 4y agoCan 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?