I find the concept of a context structure passed as the first parameter to all your functions with all your "globals" to be very compelling for this sort of stuff.
This is very similar to dependency injection. Separating state and construction from function or method implementation makes things a lot easier to test. In my opinion it's also easier to comprehend what the code actually does.
That just seems like globals with extra steps. Suddenly if your context structure has a weird value in it, you’ll have to check every function to see who messed it up.
First, that's true for globals as well. Second, with "context structure" pattern, the modifications to it are usually done by copying this structure, modifying some fields in the copy and passing the copy downwards, which severely limits the impact radius and simplifies tracking down which function messed it up: it's either something above you in the call stack or one of the very few (hopefully) functions that changes this context by-reference, with intent to apply such changes globally.
This plus immutable data is what makes doing web apps in Elixir using Phoenix so nice. There is a (demi-)god "%Conn" structure passed as the first parameter that middleware and controller actions can update (by returning a new struct). The %Conn structure is then used in the final step of the request cycle to return data for the request.
For non-web work genservers in Elixir have immutable state that is passed to every handler. This is "local" global state and since genservers guarantee ordering of requests via the mailbox handlers can update state also by returning a new state value and you never have race conditions.
That's exactly why I used this specific example. I seen many code bases that use clone to avoid mutation problems so I wrote this specifically to show it can become a problem too.
I wrote a better article on globals. I plan on posting it next week
This seems more an issue with not understanding structuralClone, than one of understanding globals or lack thereof. There’s nothing wrong with the example, it does exactly what the code says it should — if you want counter to be “global” then structuralClone isn’t the function you want to call. The bug isn’t in how counter was in obj, the bug is in calling structuralClone when its behaviour wasn’t wanted.
With that said, it seems obvious that if you want to globally count the calls, then that count shouldn’t live in an argument where you (the function) don’t control its lifetime or how global it actually is. Simple has no say over what object obj.counter points to, it could trivially be a value type passed into that particular call, so if you know you want a global count then of course storing it in the argument is the wrong choice.
Global has two conflated meanings: global lifetime (ie lifetime of the whole program) and global access (which the article states). Simple needs global lifetime but not global access.
You rarely ”need” global access, although for things like a logger it can be convenient. Often you do need global lifetime.
If I have 500 functions, I don't want to extrapolate out the overhead of passing a state object around to all of them. That's a waste of effort, and frankly makes me think you want to code using an FP paradigm even in imperative languages.
Module-level and thread-level "globals" are fine. You gain nothing (other than some smug ivory tower sense of superiority) by making your functions pure and passing around a global state object to every single method invocation.
If that’s so useful, make your language support the concept of lexical environments instead. Otherwise it’s just manual sunsetting every day of week. Our craft is full of this “let’s pretend we’re in a lisp with good syntax” where half of it is missing, but fine, we’ll simulate it by hand. Dirt and sticks engineering.
(To be clear, I’m just tangentially ranting about the state of things in general, might as well post this under somewhere else.)
You can have it either way, it’s not for you but for people who disagree with what they deem a preference that is the only option when there’s no alternative.
I got into this argument with my former coworkers. Huge legacy codebase. Important information (such as the current tenant of our multi-tenant app) was hidden away in thread-local vars. This made code really hard to understand for newcomers because you just had to know that you'd have to set certain variables before calling certain other functions. Writing tests was also much more difficult and verbose. None of these preconditions were of course documented. We started getting into more trouble once we started using Kotlin coroutines which share threads between each other. You can solve this (by setting the correct coroutine context), but it made the code even harder to understand and more error-prone.
I said we should either abolish the thread-local variables or not use coroutines, but they said "we don't want to pass so many parameters around" and "coroutines are the modern paradigm in Kotlin", so no dice.
You know what helps manage all this complexity and keep the state internally and externally consistent?
Encapsulation. Provide methods for state manipulation that keep the application state in a known good configuration. App level, module level or thread level.
Use your test harness to control this state.
If you take a step back I think you’ll realize it’s six of one, half dozen of the other. Except this way doesn’t require manually passing an object into every function in your codebase.
These methods existed. The problem was that when you added some code somewhere deep down in layers of layers of business code, you never knew whether the code you'd call would need to access that information or whether it had already previously been set.
Hiding state like that is IMHO just a recipe for disaster. Sure, if you just use global state for metrics or something, it may not be a big deal, but to use it for important business-critical code... no, please pass it around, so I can see at a glance (and with help from my compiler) which parts of the code need what kind of information.
I’m having a difficult time understanding the practical difference between watching an internal state object vs an external one. Surely if you can observe one you can just as easily observe the other, no?
Surely if you can mutate a state object and pass it, its state can get mutated equally deep within the codebase no different than a global, no?
What am I missing here? To me this just sounds like a discipline issue rather than a semantic one.
> To me this just sounds like a discipline issue rather than a semantic one.
Using an explicit parameter obviates the need for discipline since you can mechanically trace where the value was set. In contrast, global values can lead to action at a distance via implicit value changes.
For example, if you have two separate functions in the same thread, one can implicitly change a value used by the other if it's thread-local, but you can't do that if the value is passed via a parameter.
> These would be just as traceable in your IDE/debugger.
A debugger can trace a single execution of your program at runtime. It can't statically verify properties of your program.
If you pass state to your functions explicitly instead of looking it up implicitly, even in dynamically typed languages there are linters that can tell you that you've forgot to set some state (and in statically typed languages, it wouldn't even compile).
If your global state contains something that runs in prod but should not run in a testing environment (e.g. a database connection), your global variable based code is now untestable.
Dependency Injection is popular for a very good reason.
Sure. And a million programmers have all screamed out in horror when they realize that their single test passes, but fails when run as part of the whole suite. Test contamination is a hell paved with global variables.
Just need to make sure your module doesn't get too big or unwieldy. I work in a codebase with some "module" C files with a litany of global statics and it's very difficult to understand the possible states it can be in or test it.
I agree that so long as overall complexity is limited these things can be OK. As soon as you're reading and writing a global in multiple locations though I would be extremely, extremely wary.
I did not say you are targeting OP. I meant that you are degrading your parent commenter.
This:
"You gain nothing (other than some smug ivory tower sense of superiority) by making your functions pure and passing around a global state object to every single method invocation."
...is neither productive nor actually true. But I'll save the latter part for your other reply.
I could not initially reply to you. Your comment rubbed me the wrong way, because I had no intention of trying to degrade anyone, and frankly, I was offended. But I thought better of my hasty and emotional response. I would rather take a deep breath, re-focus, re-engage, and be educated in a thoughtful dialog than get into a mud slinging contest. I am always willing to be enlightened.
A tip, in your profile you can set a delay which is a number of minutes before your comments will become visible to other people. Mine is set to 2 right now. This gives you time to edit your comment (helpful for some longer ones) but also to write some garbage response and then think better and delete it before anyone's the wiser.
It's also helpful to give you time to re-read a mostly good, but maybe not polite, response and tone down your response.
"You gain nothing (other than some smug ivory tower sense of superiority) by making your functions pure and passing around a global state object to every single method invocation."
So, we already cleared it up that using that tone is not inviting discussion and shows emotional bias and that has no place in technical discussions, I believe. You said you are open to have your mind changed. Let me give you a few loosely separate (but ultimately bound to each other) arguments in favor of passing around state to each function individually.
- All functions that operate on such state are trivially testable in isolation. This is not a theoretical benefit, I've experienced it hundreds of times ever since I started working mainly with Elixir for almost 9 years now (though I still code Golang and Rust). The amount of bugs I ended up being paid to fix was confusing even to me, just for utilizing this one way of working.
- Explicit dependencies, though this one is muddy because f.ex. in Elixir that's strongly but dynamically typed this benefit is nearly non-existent; I am talking mostly about statically typed languages here, especially Rust. If you have to operate on stuff that implements this or that trait then that's a very clear contract and the code becomes that much clearer with scarcely any need for documenting those functions (though docs do help in other ways there f.ex. "how do we use this so it's, you know, useful" -- but that still means that you get to skip documenting trivia which is still a win).
- LANGUAGE-DEPENDENT: ability to construct pipes (specific to Elixir, OCaml, F# and probably a few others). Consider this:
...while passing around the same state (piping passes the first function argument down akin to currying) makes for a super terse and readable code. It was and still is a game changer for many. Piping is what I sorely miss in Golang and Rust; gods, the code is so much uglier without it though their method chaining gets you almost there as well -- fair is fair.
Also, piping almost completely negates the inconvenience that you hinted at.
- Generally giving you a better idea of the dependency graph in your code. Again, a game changer for me. In my 9 years with Java this was my biggest pain. At one point you just give up and start throwing crap at the wall until something works (or doesn't). Not that I did mind the longer dev times of Java but the productivity was just abysmal with all the DI frameworks. I am aware that things improved since then but back when I finally gave up on Java back in 2009-2011 (gradually) it was still terrible.
OK, I don't have much to go on from your otherwise fairly small comment and I already extrapolated quite a lot. Let me know what you think.
But, one rule: "I don't like it" is not allowed. It's not about "liking" stuff, it's about recognizing something that helps productivity and increases clarity.
Even in a single-threaded environment, passing a struct around containing values allows you to implement dynamic scoping, which in this case means, you can easily call some functions with some overridden values in the global struct, and that code can also pass around modified versions of the struct, and so on, and it is all cleanly handled and scoped properly. This has many and sundry uses. If you just have a plain global variable this is much more difficult.
Although Perl 5 has a nice feature where all global variables can automatically be treated this way by using a "local" keyword. It makes the global variables almost useful, and in my experience, makes people accidentally splattering globals around do a lot less damage than they otherwise would because they don't have to make explicit provision for scoped values. I don't miss many features from Perl but this is on the short list.
And do you always know beyond any reasonable doubt that your code will be single-threaded for all time? Because the moment this changes, you're in for a world of pain.
Wrapping the globals into to a struct context #ifdef MULTI-THREADED and adding this ctx for each call as first call is a matter of minutes. I've done this multiple times.
Much worse is protecting concurrent writes, eg to an object or hash table
If the discussion here is meant to be solely about JavaScript, then I'll happily consider all my comments in this thread to be obsolete, since I don't have particularly strong opinions about that language since I don't use it a lot.
I was under the impression that many people here were discussing the usage of global variables more generally, though.