7 ms·
This is about an explicit argument of type "Context". I'm not a Go user, and at first I thought it was about something else: an implicit context variable that a
by captainmuon 2y ago
This is about an explicit argument of type "Context". I'm not a Go user, and at first I thought it was about something else: an implicit context variable that allows you to pass stuff deep down the call stack, without intermediate functions knowing about it.
React has "Context", SwiftUI has "@Environment", Emacs LISP has dynamic scope (so I heard). C# has AsyncLocal, Node.JS AsyncLocalStorage.
This is one of those ideas that at first seem really wrong (isn't it just a global variable in disguise?) but is actually very useful and can result in cleaner code with less globals or less superfluous function arguments. Imagine passing a logger like this, or feature flags. Or imagine setting "debug = True" before a function, and it applies to everything down the call stack (but not in other threads/async contexts).
Implicit context (properly integrated into the type system) is something I would consider in any new language. And it might also be a solution here (altough I would say such a "clever" and unusual feature would be against the goals of Go).
- crowcountry 2y agoScala has implicit contextual parameters: https://docs.scala-lang.org/tour/implicit-parameters.html https://docs.scala-lang.org/tour/implicit-parameters.html.
- agumonkey 2y agoI've always been curious about how this feature ends up in day to day operations and long term projects. You're happy with it ?
- kloop 2y agoAs a veteran of a large scala project (which was re-written in go, so I'm not unbiased), no. I was generally not happy. This was scala 2, so implicit resolution lookup was a big chunk of the problem. There's nothing at the call site that tells you what is happening. But even when it wasn't hidden in a companion object somewhere, it was still difficult because every import change had to be scrutinized as it could cause large changes in behavior (this caused a non-zero number of production issues). They work well for anything you would use environment variables for, but a chunk of the ecosystem likes to use them for handlers (the signature being a Functor generally), which was painful
- agumonkey 2y agothanks a lot for your answer
- lmm 2y ago> There's nothing at the call site that tells you what is happening. A decent IDE highlights it at the call site. It's definitely an abusable feature, but I find it very useful. In most other languages you end up having to have completely invisible parameters (e.g. database session bound to the thread) because it would be too cumbersome to pass them explicitly. In Scala you have a middle ground option between completely explicit and completely invisible.
- agumonkey 2y agoyeah that's what i thought, but maybe scala implicit param not being perfect will help finding a better linguistic trait (maybe they should enforce purity on these parameters)
- lmm 2y agoIMO it is perfect, or at least better than anything else that's been found so far. "Purity" means different things in different contexts. Ultimately you can give programmers more tools, but you can't get away from relying on their good judgement.
- kloop 2y agoI'm not sure what you consider a decent scala ide, but it was a problem with IntelliJ in several of our code bases, and I'd have to crawl the implicit resolution path. I eventually opted to desugaring the scala completely, but we were already on the way out of scala by that point
- lmm 2y ago> it was a problem with IntelliJ in several of our code bases It shouldn't be, unless you were using macros (always officially experimental) or something - I was always primarily an Eclipse guy but IntelliJ worked well. Did you not get the green underline?
- xmodem 2y agoNot OP, but I briefly seconded to a team that used Scala at a big tech co and I was often frustrated by this feature specifically. They had a lot of code that consumed implicit parameters that I was trying to call from contexts they were not available. Then again I guess it's better than a production outage because the thread-local you didn't know was a requirement wasn't available.
- segfaltnh 2y agoScala has everything, and therefore nothing.
- sghiassy 2y agoIn Jetpack compose, the Composer is embedded by the compiler at build time into function calls https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd https://medium.com/androiddevelopers/under-the-hood-of-jetpa... I’m still not sure how I feel about it. While more annoying, I think I’d like to see it, rather than just have magic behind the hood
- fragmede 2y agoseeing it is great. coming into a hairy monolith and having to plumb one variable through half a dozen layers to get to the creamy nougat later you actually wanted it in, is not. having to do that more than once it's why they invented the "magic" implicit context variable.
- sghiassy 2y agoBut if you had to do it manually, it would presumably be plumbed everywhere to start with. And if it wasn’t immediately available, it would give you a strong signal to wonder what you’re doing wrong
- kgeist 2y agoPassing the current user ID/tenant ID inside ctx has been super useful for us. We’re already using contexts for cancellation and graceful termination, so our application-layer functions already have them. Makes sense to just reuse them to store user and tenant IDs too (which we pull from access tokens in the transport layer). We have DB sharding, so the DB layer needs to figure out which shard to choose. It does that by grabbing the user/tenant ID from the context and picking the right shard. Without contexts, this would be way harder—unless we wanted to break architecture rules, like exposing domain logic to DB details, and it would generally just clutter the code (passing tenant ID and shard IDs everywhere). Instead, we just use the "current request context" from the standard lib that can be passed around freely between modules, with various bits extracted from it as needed. What’s the alternatives, though? Syntax sugar for retrieving variables from some sort of goroutine-local storage? Not good, we want things to be explicit. Force everyone to roll their own context-like interfaces, since a standard lib's implementation can't generalize well for all sitiations? That’s exactly why contexts we introduced—because nobody wanted to deal with mismatched custom implementations from different libs. Split it into separate "data context" and "cancellation context"? Okay, now we’re passing around two variables instead of one in every function call. DI to the rescue? You can hide userID/tenantID with clever dependency injection, and that's what we did before we introduced contexts to our codebase, but that resulted in allocations of individual dependency trees for each request (i.e. we embedded userID/tenantID inside request-specific service instances, to hide the current userID/tenantID, and other request details, from the domain layer to simplify domain logic), and it stressed the GC.
- vbezhenar 2y agoAn alternative is to add all dependencies explicitly into function argument list or object fields, instead of using them implicitly from the context, without documentation and static typing. Including logger.
- kgeist 2y agoI already talked about it above. Main problems with passing dependencies in function argument lists: 1) it pollutes the code and makes refactoring harder (a small change in one place must be propagated to all call sites in the dependency tree which recursively accept user ID/tenant ID and similar info) 2) it violates various architectural principles, for example, from the point of view of our business logic, there's no such thing as "tenant ID", it's an implementation detail to more efficiently store data, and if we just rely on function argument lists, then we'd have to litter actual business logic with various infrastructure-specific references to tenant IDs and the like so that the underlying DB layer could figure out what to do. Sure, it can be solved with constructor-based dependency injection (i.e. request-specific service instances are generated for each request, and we store user ID/tenant ID & friends as object fields of such request-scoped instances), and that's what we had before switching to contexts, but it resulted in excessive allocations and unnecessary memory pressure for our highload services. In complex enterprise code, those dependency trees can be quite large -- and we ended up allocating huge dependency trees for each request. With contexts, we now have a single application-scoped service dependency tree, and request-specific stuff just comes inside contexts. Both problems can be solved by trying to group and reuse data cleverly, and eventually you'll get back to square one with an implementation which looks similar to ctx.Context but which is not reusable/composable. >Including logger. We don't store loggers in ctx, they aren't request-specific, so we just use constructor-based DI.
- TeMPOraL 2y ago> React has "Context", SwiftUI has "@Environment", Emacs LISP has dynamic scope (so I heard). C# has AsyncLocal, Node.JS AsyncLocalStorage. Emacs Lisp retains dynamic scope, but it's no longer a default for some time now, in line in other Lisps that remain in use. Dynamic scope is one of the greatest features in Lisp language family, and it's sad to see it's missing almost everywhere else - where, as you noted, it's being reinvented, but poorly, because it's not a first-class language feature. On that note, the most common case of dynamic scope that almost everyone is familiar with, are environment variables. That's what they're for. Since most devs these days are not familiar with the idea of dynamic scope, this leads to a lot of peculiar practices and footguns the industry has around environment variables, that all stem from misunderstanding what they are for. > This is one of those ideas that at first seem really wrong (isn't it just a global variable in disguise?) It's not. It's about scoping a value to the call stack. Correctly used, rebinding a value to a dynamic variable should only be visible to the block doing the rebinding, and everything below it on the call stack at runtime. > Implicit context (properly integrated into the type system) is something I would consider in any new language. That's the problem I believe is currently unsolved, and possibly unsolvable in the overall programming paradigm we work under. One of the main practical benefits of dynamic scope is that place X can set up some value for place Z down on the call stack, while keeping everything in between X and Z oblivious of this fact. Now, this is trivial in dynamically typed language, but it goes against the principles behind statically-typed languages, which all hate implicit things. (FWIW, I love types, but I also hate having to be explicit about irrelevant things. Since whether something is relevant or not isn't just a property of code, but also a property of a specific programmer at specific time and place, we're in a bit of a pickle. A shorter name for "stuff that's relevant or not depending on what you're doing at the moment" is cross-cutting concerns, and we still suck at managing them.)
- masklinn 2y ago> Emacs Lisp retains dynamic scope, but it's no longer a default for some time now https://www.gnu.org/software/emacs/manual/html_node/elisp/Variable-Scoping.html https://www.gnu.org/software/emacs/manual/html_node/elisp/Va... > By default, the local bindings that Emacs creates are dynamic bindings. Such a binding has dynamic scope, meaning that any part of the program can potentially access the variable binding. It also has dynamic extent, meaning that the binding lasts only while the binding construct (such as the body of a let form) is being executed. It’s also not really germane to the GP’s comment, as they’re just talking about dynamic scoping being available, which it will almost certainly always be (because it’s useful).
- whstl 2y agoYeah, I agree 100% with you. The thing with Golang is that it's supposed to be a very explicit language, so passing the context as an argument fits in with the rest of the language. Nevertheless: just having it, be it implicit or explicit, beats having to implement it yourself.
- segfaltnh 2y agoThread local storage means all async tasks (goroutines) must run in the same thread. This isn't how tasks are actually scheduled. A request can fan out, or contention can move parts of the computation between threads, which is why context exists. Furthermore in Go threads are spun up at process start, not at request time, so thread-local has a leak risk or cleanup cost. Contexts are all releasable after their processing ends. I've grown to be a huge fan of Go for servers and context is one reason. That said, I agree with a lot of the critique and would love to see an in-language solution, but thread-local ain't it.
- cyberax 2y agoA more correct term is "goroutine-local" storage, which Go _already_ has. It's used for pprof labels, they are even inherited when a new Goroutine is started.
- deleted 2y ago[deleted]
- karolinepauls 2y agoA good pitch for dynamic (context) variables is that they're not globals, they're like implicit arguments passed to all functions within the scope. Personally I've used the (ugly) Python contextvars for: - SQS message ID in to allow extending message visibility in any place in the code - scoped logging context in logstruct (structlog killer in development :D) I no longer remember what I used Clojure dynvars for, probably something dumb. That being said, I don't believe that "active" objects like DB connection/session/transaction are good candidates for a context var value. Programmers need to learn to push side effects up the stack instead. Flask-SQLAlchemy is not correct here. Even Flask's request object being context-scoped is a bad thing since it is usually not a problem to do all the dispatching in the view.
- lmm 2y ago> Implicit context (properly integrated into the type system) is something I would consider in any new language. Those who forget monads are doomed to reinvent dozens of limited single-purpose variants of them as language features.
- mananaysiempre 2y agoAlgebraic effects and implicit arguments with explicit records are perfectly cromulent language features. GHC Haskell already has implicit arguments, and IIRC Scala uses them instead of a typeclass/trait system. The situation with extensible records in Haskell is more troublesome, but it’s more because of the endless bikeshedding of precisely how powerful they should be and because you can get almost all the way there with the existing type-system features except the ergonomics invariably suck. It’s reasonable, I think, to want the dynamic scope but not the control-flow capabilities of monads, and in a language with mutability that might even be a better choice. (Then again, maybe not—SwiftUI is founded on Swift’s result builders, and those seem pretty much like monads by another name to me.) And I don’t think anybody likes writing the boilerplate you need to layer a dozen MonadReaders or -States on each other and then compose meaningful MonadMyLibraries out of them. Finally, there’s the question of strong typing. You do want the whole thing to be strongly typed, but you don’t want the caller to write the entire dependency tree of the callee, or perhaps even to know it. Yet the caller may want to declare a type for itself. Allowing type signatures to be partly specified and partly inferred is not a common feature, and in general development seems to be backing away from large-scale type inference of this sort due to issues with compile errors. Not breaking ABI when the dependencies change (perhaps through default values of some sort) is a more difficult problem still. (Note the last part can be repeated word for word for checked exceptions/typed errors. Those are also, as far as I’m aware, largely unsolved—and no, Rust doesn’t do much here except make the problem more apparent.)
- cesarb 2y ago> an implicit context variable that allows you to pass stuff deep down the call stack, without intermediate functions knowing about it. [...] but is actually very useful and can result in cleaner code with less globals or less superfluous function arguments. [...] and it applies to everything down the call stack (but not in other threads/async contexts). In my experience, these "thread-local" implicit contexts are a pain, for several reasons. First of all, they make refactoring harder: things like moving part of the computation to a thread pool, making part of the computation lazy, calling something which ends up modifying the implicit context behind your back without you knowing, etc. All of that means you have to manually save and restore the implicit context (inheritance doesn't help when the thread doing the work is not under your control). And for that, you have to know which implicit contexts exist (and how to save and restore them), which leads to my second point: they make the code harder to understand and debug. You have to know and understand each and every implicit context which might affect code you're calling (or code called by code you're calling, and so on). As proponents of another programming language would say, explicit is better than implicit.
- mst 2y agoThey're basically dynamic scoping and it's both a very useful and powerful and very dangerous feature ... scheme's dynamic-wind model makes it more obvious when the particular form of magic is in use but isn't otherwise a lot different. I would like to think that somebody better at type systems than me could provide a way to encode it into one that doesn't require typing out the dynamic names and types on every single function but can instead infer them based on what other functions are being called therein, but even assuming you had that I'm not sure how much of the (very real) issues you describe it would ameliorate. I think for golang the answer is probably "no, that sort of powerful but dangerous feature is not what we're going for here" ... and yet when used sufficiently sparingly in other languages, I've found it incredibly helpful. Trade-offs all the way down as ever.
- wbl 2y agoBasically you'd be asking for inferring a record type largely transparently. That's going to quickly explode to the most naive form because it's very hard to tell what could be called, especially in Go.
- flohofwoe 2y agoI haven't seen it mentioned yet, but Odin also has an implicit `context` variable: https://odin-lang.org/docs/overview/#implicit-context-system https://odin-lang.org/docs/overview/#implicit-context-system
- biodniggnj 2y agoAnd Java added ScopedValue in version 20 as a preview feature.
- dang 2y agoWe added exactly this feature to Arc* and it has proven quite useful. Long writeup in this thread: https://news.ycombinator.com/item?id=11240681 https://news.ycombinator.com/item?id=11240681 (March 2016) * the Lisp that HN is written in
- Ferret7446 2y agoWe already have implicit context. It's called thread local storage and dynamic scoping, and we figured out it's a bad idea a long time ago. Explicitly passing data and lexical scoping is better for understandability.