Hacker News new | ask | show | jobs
by br1 3096 days ago
It's indefensible that defer works on the function and not on the scope.
2 comments

Both seem like fair options to me. With function scope, you can

    func f() {
      x := ...
      if x.something() {
        x.doSomethingEarlier()
        defer x.cleanup()
      }
      // use x however you like
    }
where scope-based forces you to do stuff like

    func f() {
      x := ...
      if x.something() {
        x.doSomethingEarlier()
        defer x.cleanup()
        // use x however you like
      } else {
        // use x however you like
      }
    }
In a scope-based defer, you'd have to keep all related code in the scope, nesting it another layer deeper / possibly duplicating it.

On the flip-side is of course that this doesn't work like most would probably want in function-scoped:

    for i := 0; i < 4; i++ {
      x := get(i)
      defer x.cleanup()
      x.whatever()
    }
and you're forced to

    for i := 0; i < 4; i++ {
      x := get(i)
      func() {
        defer x.cleanup()
        x.whatever()
      }()
    }
I've seen both of these patterns pretty frequently, in Go and in other languages. Go could, of course, have both a func_defer and a scope_defer, but that doesn't seem like it'd fit with the fairly strong focus on keeping the language feeling small and simple. So they had to pick one, and it can't handle both cases.
I've never seen the first pattern in any other language. Aside from downcasts as in your other example, why would you only want to clean up an object if some condition is true? The 99% use case of defer is for resource destruction, which you nearly always want to do in the same scope the object was initialized in (and that observation in fact is what underlies RAII).
I think function scope is a good default, and wrapping in an anonymous function and calling it (like your last example) is a simple workaround to get the scope_defer behavior. If it was scope based there's nothing you could do to get func_defer behavior.
Yeah, I generally feel the same way. For fairly simple use, scope is more consistent (all scopes / closures are identical), but func is a bit more flexible if you're willing to pay with simple boilerplate.

I mean, you can convert them into each other. Scoped can do something like this (go+python blended code 'cuz lazy):

    func f(){
      deferred := []
      defer func() { for d in deferred.reverse(): d() }() // plus error handling
      if x.something() {
        deferred.push(func(){ cleanup() });
      }
      // same as func scope
    }
but that's a bit more ridiculous / error-prone (though a helper func is obviously possible) than the equivalent IIFE for func -> scope. More explicit, I suppose, but bleh.
It's more explicit, which is a good thing, as it makes the intent clear. This matters if, for example, the function is later refactored to inline into a caller.
It also allows more flexibility (do you execute them in the order they were enqueued, or in reverse?), more room for errors, confusion between different patterns / lack of consistency across different codebases, etc.

Explicit-all-the-things isn't an unambiguous Good Thing™. If it were, we wouldn't even be discussing this - it's an abstraction, which is less explicit than e.g. building defer out of a list and using GOTO.

> If it was scope based there's nothing you could do to get func_defer behavior.

That's clearly false. You could set up a list to hold objects to be disposed (or, more generally, closures to execute) and defer a simple procedure that disposes of all objects in the list. This is in fact what the implementation of defer must do internally.

I think by that he means it wouldn't be possible to get func_defer behavior in the example with a single keyword, or without some form of qualifier. The compiler wouldn't be able to differentiate the behavior.
Well I think the function scope the most useful, but wish there was block scope available too.

So what I really wish was that languages used dot notation to go up scope eg ..name is that name two blocks out. This was one of the good parts of VB syntax which I miss in other languages. Think how much nicer it is than python's nonlocal and global, for example!

What's the use case for function scoped defer? I have never once needed function scoped RAII in C++ or any other language.
It's not terribly uncommon for larger functions to do something like

    func someSQLStuff() {
        tx, err := createTx()     
   
        defer func() {
            if err != nil {
                tx.Rollback()
                log(err)
            } else {
                tx.Commit()
            }
         }()

         rows, err = tx.QueryContext( ... )

         // more SQL
    }
Basically, function-scoped cleanup. Like closing opened files.
But that defer is already lexically at the function scope, so block-scoped defer would do the same thing.
Ah, sorry, you meant function-scoped vs block-scoped not just in general. Yeah, agreed.
I have a comment at a higher level with a broader example, but for Go at least this is somewhat common:

    func f(i interface{}) {
      if closable, ok := i.(closable); ok {
        defer closable.close()
      }
      // do stuff with i, maybe other casts, etc
    }
There aren't many nice options for "if I can call X, defer a call to X" aside from shoving it into an `if`, where it'd be captured by that scope. I mean, you could do something like

    deferrable := func(){}
    if closable {
      deferrable = closable.close
    }
    defer deferrable()
but imagine doing that every time. It'd work, sure, but it'd also be more annoying.
Couldn't you do:

    func closeIfNecessary(object interface{}) {
        closable, needsClosing := object.(closable)
        if needsClosing {
            closable.close()
        }
    }
And then just do:

    func f(i interface{}) {
        defer closeIfNecessary(i)
        ...
    }
Doing it this way also saves you boilerplate by factoring the downcast out into a separate function.
As long as you can always call .close() regardless of the code below, yep - that'd work, and is definitely more readable.

If you can't call it unless [some other conditions], it goes back to the same kind of problem though. "closable" may not be a good choice on my part, as they're often called unconditionally.

Collecting tasks from all iterations to await before returning.
Fair enough. But it's something like 5 lines of code to write that manually, and writing it explicitly is clearer. With implicit function-scoped defer, someone might refactor the code to inline the body of the function into its caller and break it.
I totally agree with you.
It's true that defer is more powerful at function scope. You can always recover block scope with an unnamed func. But it doesn't fit with normal lexically scoped constructs. You get gotchas.

I would love to be able to explicitly affect other scopes as you mention, for example to define two classes at the same time.