Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Hard disagree.

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.



You get functions that are easily testable in isolation with all state provided in parameters.

You also get explicit dependencies and scoping controlled by caller.

I don't mind globals but saying you get nothing for avoiding them is :/


I tend to use getter and setter functions to access globals and manage state.

Advantage only the function that depends on the global needs to bring in the dependency.


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.)


So, making it implicit again? No.


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.


I'm sorry I just think we're going in circles.

A few posts up I clearly said use encapsulation and then mutate state via known methods. These would be just as traceable in your IDE/debugger.

Further, I have also repeatedly mentioned both module-level and thread-level "globals".

And I understand how concurrency works.

Take care.


> 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).


> 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?

Yes, that's one reason why shared mutable state should IMHO be kept to a minimum and immutable objects/structs should be preferred.

  // this function will implicitly look up the tenant from some global context
  fun createUser(username: String, password: String)
vs.

  fun createUser(username: String, password: String, tenant: Tenant)
except imagine that the comment for that first function didn't exist (because nobody documents shit anymore these days).

The second one is almost impossible to accidentally misuse, the first one can lead to all sorts of weird bugs.


Ouch, I partially address this in the next article


Yes, you gain testability.

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.


This sounds like a design deficiency.

If you have something that should only run in testing, perhaps your test harness should set the global variable appropriately, no?


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.


I can't agree more, I was dying this death a thousand times over at my last job.


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.


It's one register. You gain that much performance from the single optimization of not updating rbp in release builds.


Hard to take your comment seriously when you go out of your way to degrade a discussion opponent, FYI.


My comment was not intended to be personally degrading to OP. Apologies if it was taken that way.


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'll take my medicine. :)

I'm not above being put in my place and being shown the light.

(And when I said OP, I did mean the parent poster)

You should also understand that the "you" in my comment you quoted is the colloquial you and not necessarily the parent poster.

So lay it on me.


OK, I will.

But I also got a notification about a big comment you posted but it is now deleted. Did you want to rewrite it or did you give up on it?


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.


Much appreciated :)


That is a good tip.


Commendable, still let's not forget that the part I quoted was the beginning of a mud-slinging contest that you seemed willing to start. ;)

So indeed, let's re-focus and re-engage.

To that end, I will post you a longer reply Later™.


Alright now. Let's have a quick go at 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."

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:

    websocket
    |> check_for_bearer_token(req.headers)
    |> authenticate()
    |> authorize(can_edit_orders?())
...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.




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: