Hacker News new | ask | show | jobs
by culturedsystems 1461 days ago
If your code is sufficiently distinct that block scope is useful, and there's no obvious contextual clue as to why it's distinct (like an if statement), put it in a separate function to explain why it's distinct. The article mentions this argument, but responds with some handwaving about how technically it's valid JS to use an unnamed block statement. I mean, sure, but does that mean it's a good idea?
5 comments

> but does that mean it's a good idea?

Yes, to me it does. I love when variables have really tight scopes so I don't need to worry about if/how they are used throughout the rest of the program. Locality should be the default, not something that needs to be justified.

Creating a function is just unnecessary boilerplate if the code is not going to be reused. Functions are just named blocks with arguments. But if you don't need to name the block and don't need to reuse it with varying arguments, creating a function is just unnecessary.

Functional programmers are familiar with a concept called "let", which essentially gives you a way to introduce a variable and introduce a new scope just for that variable. This article is essentially just showing how to do that in JavaScript.

I've used this method in C code. One reason---C89 only allows variable declarations at the top of a block, and if I have a small section of code towards the middle of end of the function with its own variable use, the declaration is separated from the use. By using a new scope, the declaration can be done right where it's used. Yes, it looks a bit weird, but there could be benefits.

I also use this in Lua. The benefit there---once the scope is over, the variables declared in the scope become available for garbage collection. In a small function, this isn't that much of a win, but for the top level scope, it probably is. You can see an example of this in my gopher server [1]. Don't let the formatting confuse you, this:

    local CONF = {} do
      local conf,err = loadfile(arg[1],"t",CONF)
      if not conf then
        io.stderr:write(string.format("%s: %s\n",arg[1],err))
        os.exit(exit.CONFIG,true)
      end
      -- rest of code
    end
is the same as

    local CONF = {} 
    do
      local conf,err = loadfile(arg[1],"t",CONF)
      if not conf then
        io.stderr:write(string.format("%s: %s\n",arg[1],err))
        os.exit(exit.CONFIG,true)
      end
      -- rest of code
    end
The 'do' keyword introduces a new scope (and can be used anywhere to do so). I format it as the former to be more explicit about the CONF variable being defined by the following code block.

[1] https://github.com/spc476/port70/blob/master/port70.lua#L41

Oh, one other reason to do the new scope instead of a function---you can still use variables in the outer scope that you might otherwise have to pass in to a function.
A separate function requires a mental "push/pop context" when reading the code to find and read the called function and then returning to the caller function, a simple scope block doesn't require such mental stack gynmastics (yes, some IDEs can help by visually inlining the called function back into the caller function, but that's just undoing an action that probably shouldn't have happened in the first place).
I have a long but pretty linear function whose job it is to take a whole mess of data and build a SAT model. Even with even with jump-to-definition etc lots of little[1] sub-functions make the coffee difficult to read. I would kill for block scope (yay python) to ensure that I'm not accidentally reusing a temporary variable from three steps ago.

[1] honestly, not so little

Hmm. Now you've got me thinking about the most ergonomic way do define a block in python. Maybe a no-op context manager? So you could do "with scope():"

I admit to using block scopes in rust a fair bit

Ignoring any future/proposed syntax, I think the most block-like would be a local function definition which you then call once right afterward. This is the same you would do in any LISP-like language.

It could take zero arguments if you just want to temporarily extend the current scope with a few more variable-binding statements. Or, it could also have arguments if you want to do a "let" like construct to rename some expression results in the outer (calling) scope during the invocation.

  # ... outer scope
  x = expr1
  y = expr2
  r1 = None
  r2 = None
  
  def block1():
    nonlocal r1
    z = expr3
    r1 = expr4
  block1()
  
  def block2(x, y):
    nonlocal r2
    z = expr5
    r2 = expr6
  block2(expr7, expr8)
Edited for typos, code formatting, and bugs ;-).
Plain context manager doesn't actually introduce a new scope:

    with open("foo") as f:
        s = f.read()
    print(f) # closed file object still exists
A macro that looks like a context manager would work but beware the edge cases.