Hacker News new | ask | show | jobs
by jolmg 2100 days ago
> If I ever saw anyone I worked with trying to do something like "setting a global variable to different things in different threads", I would have very strong words for them.

Because global variables aren't used the same between the two types of languages. In lexically-scoped languages, it's bad practice overall to use global variables for anything except constants. In dynamically-scoped languages, they're used as an environment just like how environment variables are used among processes.

You asked why not use "normal" (statically-scoped) global variables, and I replied on the difference. That doesn't mean that I support using them like that in such languages.

Imagine environment variables didn't exist. A `su` command that modified the home directory in /etc/passwd for the duration of its subprocess to change it back when it dies would also seem pretty ugly for me. Indeed, if a language lacks dynamic scoping or environment variables didn't exist, the proper practice would be to pass the whole environment explicitly as arguments. That's what's typically done in statically-scoped languages, but it has the caveats I mentioned in this other comment:

https://news.ycombinator.com/item?id=24545180

> I'm having trouble making sense of your first paragraph.

Here's an example using Elisp:

  ; Turning off static-scoping
  (setq lexical-binding nil)

  ;; Bad practice

  (defun foo ()
    bar)

  (foo)
  ;=> Debugger entered--Lisp error: (void-variable bar)

  (let ((bar 3))
    (foo))
  ;=> 3

  ;; Good practice

  (defvar bar 2)

  (foo)
  ;=> 2

  (let ((bar 3))
    (foo))
  ;=> 3
1 comments

To add to what I said yesterday, focusing on this question:

> Is there any way in which doing so via "dynamic scoping" makes it any less of a horrible idea?

In both types of languages it's a horrible thing to set a global variable, because the modification is visible to all code that runs later. If you want to know what value a global variable would have in a function, you'd need to be aware of all code that executed before.

In lexically-scoped languages, you can only read or set a variable. If you forbid setting it, you can only read it. In dynamically-scoped languages you can not only read or set a variable, you can also shadow it. Shadowing is not as bad as setting, because the "modification" is scoped to a call. Because of that, it doesn't undergo changes across functions that don't have a caller/callee relationship (e.g. cousin functions in a call-graph), and I believe that's where the real devil in modifying global variables lies, in reading a variable that could have been modified by functions that don't have a clear relationship with the current one.

Having said that, shadowing has its place. It certainly can be misused like any other feature, but I'm just answering why it's less of a horrible idea, as you put it. Though, it certainly has its benefit with no equal. Not even Haskell's Reader typeclass, because of its static-typed nature, offers exactly the same ability to modify the behavior of any callee-descendant function, unforeseen by the author of the function you're calling.

It occurs to me that this is also similar to the ability in OOP to inherit from a class or module and overriding one of its methods, only that the change, instead of being scoped to the callee-descendants, is scoped to the other methods (and their callers) that use the overriden method.