Hacker News new | ask | show | jobs
by gw 1942 days ago
Nim's defer just wraps the current scope in a try/finally, with the deferred code running in the finally. It is probably better just to use try/finally directly because it's more explicit about what is in the try block. It's not worth it to obscure that just to avoid a new level of indentation...
2 comments

To me, the benefit of defer over try/finally is code locality: You can easily see whether some action has an associated cleanup, because the 'defer' statement is just before/after the action. With try/finally, the cleanup (which has to be in sync with the action) might be outside your editor window.
Came here to note this exact difference with respect to finally

Consider this code

    fp = os.open(x) // imagine file open
    defer fp.close()
    fp.read()
With defer, one would think I can simply wrap this in a for-loop if I want to open and read a bunch of files. Go doesn’t promise this, but not clear until linter complains. In languages were it “ends at scope”, this is still wrong. If we wrote it as finally, dev would know finally is outside the loop or they need to wrap in another sub-scope inside the loop {}
In languages were it “ends at scope”, this is still wrong.

I don’t quite follow -- what would go wrong? If you put that in a loop body, won’t you get one open() and one close() per iteration, as intended?

Not the commenter but which one of these is the defer generating?

    // option 1
    try {
      fp = os.open(x)
      fp.read()
    }
    finally {
      fp.close()
    }

    // option 2
    fp = os.open(x)
    try {
      fp.read()
    }
    finally {
      fp.close()
    }
Should we close if open fails? Maybe, maybe not, but with try/finally it is obvious which one it is doing.
The first one is obviously wrong, which I mean in the sense that anyone who knows what defer is will know that, not in the sense that you've posted an obviously bad post (it is a fair question!). defer is a statement, not a declaration, and does not take effect until it is executed like any other statement. It follows standard structured programming rules; line 2 does not execute until line 1 is done. (A rule so simple and obvious we often don't think about, especially since structured programming has basically won and everything we use nowadays is structured, but it is still a rule that we use in programming.)
The first one doesn't compile in any language that requires variables to be initialized before use.
In nim, it generates code analogous to option 2.
You sure? https://forum.nim-lang.org/t/4022#25046 edit: the docs seem to say you're right, maybe that forum post is outdated but it's from nim's author.
What I meant was if I am Go developer and I write

    for /* whatever */ { 
       fp = os.open(x) // imagine file open
       defer fp.close()
       fp.read()
    } 

    This is wrong. Go linter might tell you it is wrong, but

      a) the defers are pilling up and you might run into too-many-files open
      b) I can never remember if `fp` is saved in defer closure by reference or value. That is, at the end, even if inefficient, are all pending defers closing the same pointer?
Now in a language where "it ends in scope", the scope hasn't ended until the loop exists. Now in the RAII world, the `fp` being overwritten would have saved the day by automatically closing it, but we are not talking about RAII world.

With finally, things are clear, I think.

Now in a language where "it ends in scope", the scope hasn't ended until the loop exists. (I assume you mean exits there?)

No, I don’t think that’s correct, in any mainstream language. Each iteration of the loop body is a separate scope. If you declare a new variable inside the loop body, it lives until the end of that iteration then falls out of scope. Next time around the loop, you declare a new, separate variable.

There is a slight grey area around the loop header -- exactly when does the scope start and end? Older C compilers used to disagree about this, but the rules were firmed up in C++ (and I assume in recent versions of C too) and now loop headers use the tightest scope they can.

So I would expect “defer” to run at the end of each iteration, exactly the same as the C++-style RAII case, and that is in fact how it works in every modern language except Go.

Another way to think about it that might be helpful: most languages try to implement defer in a completely static way, where just looking at the syntax, you can figure out exactly where and when defer handlers are going to run. You can allocate all the storage you need at the start of the function, and nothing tricky is required at runtime. If defer handlers are queued up and run as a batch later on, that’s dynamic behavior that needs some extra runtime support, and that’s why most languages don’t do it.

> Each iteration of the loop body is a separate scope.

Thanks for the correction. You are right. I wasn’t thinking straight.