by levodelellis on 1/31/25, 8:04 PM with 158 comments
by Jtsummers on 2/3/25, 12:57 AM
Never tie global state information to ephemeral objects whose lifetime may be smaller than what you want to track. In this case, they want to know how many times `simple` is called across the program's lifetime. Unless you can guarantee the `obj` argument or its `counter` member exists from before the first call to `simple` and through the last call to `simple` and is the only `obj` to ever be passed to `simple`, it is the wrong place to put the count information. And with those guarantees, you may as well remove `obj` as a parameter to both `simple` and `complex` and just treat it as a global.
State information needs to exist in objects or locations that last as long as that state information is relevant, no more, no less. If the information is about the overall program lifecycle, then a global can make sense. If you only need to know how many times `simple` was invoked with a particular `obj` instance, then tie it to the object passed in as the `obj` argument.
by billforsternz on 2/3/25, 1:35 AM
These variables are not on the heap. They are statically allocated. Their visibility is the only thing that differentiates them from global variables and static variables defined outside of functions.
I think such variables can be useful, if you need a simple way of keeping some persistent state within a function. Of course it's more state you are carrying around, so it's hard to defend in a code review as best practice.
Amusingly, you can modify such variables from outside the function, simply by getting your function to provide the modifying code with a pointer to the variable, eg by returning the variable's address. If you do that though you're probably creating a monster. In contrast I think returning the value of the static variable (which the author casts shade on in the quote above) seems absolutely fine.
Edit: I should have stated that the number one problem with this technique is that it's absolutely not thread safe for obvious reasons.
by hansvm on 2/3/25, 1:24 AM
1. They work against local reasoning as you analyze the code
2. The semantic lifetime for a bundle of data is rarely actually the lifetime of the program
The second of those is easy to guard against. Just give the bundle of data a name associated with its desired lifetime. If you really only need one of those lifetimes then globally allocate one of them (in most languages this is as cheap as independently handling a bunch of globals, baked into the binary in a low-level language). If necessary, give it a `reset()` method.
The first is a more interesting problem. Even if you bundle data into some sort of `HTTPRequest` lifetime or whatever, the fact that it's bundled still works against local reasoning as you try to use your various counters and loggers and what have you. It's the same battle between implicit and explicit parameters we've argued about for decades. I don't have any concrete advice, but anecdotally I see more bugs from biggish collections of heterogeneous data types than I do from passing everything around manually (just the subsets people actually need).
by darioush on 2/3/25, 2:09 PM
For example, global variables have drawbacks, but so does re-writing every function in a call-stack (that perhaps you don't control and get callbacks from).
Or if you are building a prototype, the magic of being able to access "anything from anywhere" (either via globals or context, which is effectively a global that's scoped to a callstack), increases your speed by 10x (since you don't have to change all the code to keep passing its own abstractions to itself as arguments!)
Functions with long signatures are tedious to call, create poor horizontal (which then spills over to vertical) code density. This impacts your ability to look at code and follow the logic at a glance, and perhaps spot major bugs in review. There's also fewer stuff for say copilot to fill in incorrectly, increasing your ability to use AI.
At the end, every program has global state, and use of almost every programming construct from function calls (which may stack overflow) or the modulus operator (which can cause division by zero), or sharing memory between threads (which can cause data races) requires respecting some invariants. Instead, programmers will go to lengths to avoid globals (like singletons or other made up abstractions -- all while claiming the abstractions originate in the problem domain) to represent global state, because someone on the internet said it's bad.
by robertlagrant on 2/3/25, 11:28 AM
If you just look at what people normally mean by global variable, then I don't think the article changes minds on that.
by serbuvlad on 2/3/25, 12:13 AM
by SpicyLemonZest on 2/3/25, 12:28 AM
I agree with this, but the problem with global variables is precisely that they make bad data access patterns look easy and natural. Speaking from experience, it’s a lot easier to enforce a “no global variables” rule than explain to a new graduate why you won’t allow them to assign a variable in module X even though it’s OK in module Y.
by PeterStuer on 2/3/25, 6:46 AM
by bb88 on 2/3/25, 12:09 AM
Singletons if you must. At least you can wrap a mutex around access if you're trying to make it thread safe.
by Tainnor on 2/3/25, 7:58 AM
I'm really confused, as this behaviour appears to be completely obvious to me.
by qalmakka on 2/3/25, 6:16 AM
by Puts on 2/3/25, 12:13 AM
by js8 on 2/3/25, 7:04 AM
As Gilad Bracha has pointed out, types are antimodular, and your database schema can be considered one giant type that pervades your program, just like globals can be.
I don't think we have tools to compositionally solve this, across different programming languages.
by pdimitar on 2/3/25, 12:52 AM
I mean yes, using global variables is just one of the ways to cause action-at-a-distance and that is... apparently a big reveal?
Otherwise sure, there is no pattern that cannot be utilized 100% correctly and without introducing bugs. Theoretically. Now let's look at the practical aspects and how often indiscriminately using such tempting patterns like global variables -- and mutexes-when-we-are-not-sure and I-will-remember-not-to-mutate-through-this-pointer -- lead to bugs down the road.
The answer is: fairly often.
IMO the article would be better titled as "There is no pattern that a lazy or careless programmer cannot use to introduce a bug".
by Joel_Mckay on 2/3/25, 6:33 AM
1. Thread container registry with mutex lock for garbage collection and inter-process communication (children know their thread ID)
2. volatile system register and memory DMA use in low level cpu/mcu (the compiler and or linker could pooch the hardware memory layout)
3. Performance optimized pre-cached shared-memory state-machines with non-blocking magic
4. OS Unikernels
Not sure I have seen many other valid use-cases that splatter language scopes with bad/naive designs. YMMV =3
by AdieuToLogic on 2/3/25, 1:50 AM
With a little encapsulation, you can make globals error-proof ...
by G_o_D on 2/3/25, 4:04 AM
by grandempire on 2/3/25, 12:23 AM
by debeloo on 2/4/25, 12:32 AM
Perhaps a unicorn doesn't die as soon as you first use a global var. But it has two .45s pointed to it cranium left and right. And at any random moment Danni DeVito will start blasting.
by tedk-42 on 2/3/25, 8:25 AM
by fergie on 2/3/25, 7:52 AM
by brainzap on 2/3/25, 10:50 AM
by senderista on 2/3/25, 4:52 AM
by cies on 2/3/25, 10:11 AM
It should have been "Mutability is the Problem, Globalness is not".
by sennalen on 2/3/25, 4:21 PM
by paulsutter on 2/3/25, 4:05 AM
by anymouse123456 on 2/3/25, 1:38 PM
Moving state into the global namespace and accessing it directly from that space makes it much more difficult to test, instrument and integrate.
Sure, if you're building disposable toy software, do whatever is easiest. But if you're building software for others to use, at least provide a context struct and pass that around when you can.
For those cases where this is challenging or impossible, please sequence your application or library initialization so that these globals are at least fungible/assignable at runtime.
by stickfigure on 2/4/25, 5:15 AM
by 2d8a875f-39a2-4 on 2/3/25, 11:10 AM
by jongjong on 2/3/25, 12:15 AM
The real underlying problem is 'Spooky action at a distance'; it's not an issue that is specific to global variables. If you pass an instance between components by-reference and its properties get modified by multiple components, it can become difficult to track where the state changes originated and that can create very nasty, difficult-to-reproduce bugs. So this can happen even if your code is fully modularized; the issue is that passing instances by reference means that the properties of that instance behave similarly to global variables as they can be modified by multiple different components/files (without a single component being responsible for it).
That's partly where the motivation for functional programming comes from; it forces pass-by-value all the time to avoid all possibility of mutations. The core value is not unique to FP though; it comes from designing components such that they have a simple interface which requires mostly primitive types as parameters. Passing objects is OK too, so long as these objects only represent structured information and their references aren't being held onto for future transformation.
So for example, you can let components fully encapsulate all the 'instances' which they manage and only give those parent components INFORMATION about what they have to do (without trying to micromanage their child instances); I avoid passing instances or modules to each other as it generally indicates a leaky abstraction.
Sometimes it takes some creativity to find a solution which doesn't require instance-passing but when you find such solution, the benefits are usually significant and lasting. The focus should be on message-passing. Like when logging, the code will be easier to follow if all the errors from all the components bubble up to the main file (e.g. via events, streams, callbacks...) and are logged inside the main file because then any developer debugging the code can find the log inside the main file and then trade it down to its originating component.
Methods should be given information about what to do, they should not be given the tools to do their job... Like if you catch a taxi in real life, you don't bring a jerrycan of petrol and a steering wheel with you to give to the taxi driver. You just provide them with information; the address of your desired destination. You trust that the Taxi driver has all the tools they need to do the job.
If you do really want to pass an instance to another instance to manage, then the single-responsibility principle helps limit the complexity and possibility for spooky action. It should only be passed once to initialize and then the receiving component needs to have full control/responsibility for that child. I try to avoid as much as possible though.