The author takes great care to rebut a common theme among objections to the proposal - “this isn’t necessary if you just write code better”. I am reminded of this fantastic essay:
> If we flew planes like we write code, we’d have daily crashes, of course, but beyond that, the response to every plane crash would be: “only a bad pilot blames their plane!”
> This doesn’t happen in aviation, because in aviation we have decided, correctly, that human error is an intrinsic and inseparable part of human activity. And so we have built concentric layers of mechanical checks and balances around pilots, to take on part of the load of flying. Because humans are tired, they are burned out, they have limited focus, limited working memory, they are traumatized by writing executable YAML, etc.
> Mechanical processes are independent of the skill of the programmer. Mechanical processes scale, unlike berating people to simply write fewer bugs.
Defer is often quite nice when you have many return paths but it also seems fairly limited if our goal is to assume human error is unavoidable. Ruby block-based stuff or "with" in Python seem like the clear winner there.
- the language still allows you write the unsafe version even with defer. By your logic fallible humans will continue to write these class of bugs because they can.
- adding a whole new flow control construct will introduce a whole new class of bugs. The dog barking example is cool for demonstrating how defer works, but is completely unreadable for what it does, programmers will write code like that because they are allowed to, and unreadable code becomes buggy code.
- to make a language safer you should remove the things that make unsafe behavior possible, not add constructs which make safe behavior easier.
> The central idea behind defer is that, unlike its Go counterpart, defer in C is lexically bound, or “translation-time” only, or “statically scoped”. What that means is that defer runs unconditionally at the end of the block or the scope it is bound to based on its lexical position in the order of the program.
The only reasonable way for defer to behave. Function scoped never made sense to me given the wasted potential. The demonstration with loop and mutex being a good one.
I love the Principle of Least Astonishment, but I first encountered it in the Ruby book and I gave up reading it halfway through because I kept thinking, "He and I have very different definitions of astonishing..."
It's definitely a footgun but I think it's also pretty clear in go docs that defer is a function-return-time thing, versus a loop iteration thing. "A defer statement defers the execution of a function until the surrounding function returns." from https://go.dev/tour/flowcontrol/12
I think per scope-level is probably better, but honestly still - as a I mention elsewhere - still something that seems fairly limited compared to writing code inside blocks that clean themselves up in the Ruby world. The more we're messing with scope, the more it seems like it would be possible to go all the way to that? The go-style defer appears likely to be simpler from an implementation POV; if we're gonna make it harder let's go all the way!
I know a lot of people hate the nesting of indentation from that, but it makes so many other things harder to screw up.
My biggest astonishment is how people continue to shoot themselves in the foot by not making scope vs function declarations explicit. For the reasons that “someone will misunderstand complicated ideas, so let’s make it implicit” or something. While there could be just:
defer x // scope scoped
defer fn x // function scoped
Also:
var a = 0
fn var a = 0
for fn i := …
But we have this allergy to full control and invent these increasingly stupid ways to stay alert and get unpleasantly surprised anyway.
Edit: Same for iifes. Everyone uses them, so turn these into proper embedded blocks!
Say that the defer would execute inside for loops, what would make you more astonished: loops and functions are the exceptions, or defers execute at the end of any block? I would prefer the latter of these two. But then the consequence is that a defer in an if-block executes instantly, so you cannot conditionally defer anymore. So it seems that the rules for when deferees execute need to be arbitrary, and "only functions" seems fewer exceptions than "only functions and loops", isn't it? And what about loops implemented through gotos? Oh boy.
You can play with defer in Linux/VM with slimcc[1] today! It only diverges from the TS in keyword being _Defer, as well as several goto constraint violations not detected, bright side is you can witness why they are constraint violations...
Another cool difference between this and Go’s ‘defer’, is that it doesn’t allocate memory on the heap. Go’s ‘defer’ does and it has a small performance cost compared to just calling the .release() or whatever yourself… shrugs
At least this was the case last I did benchmarks of my Go code. Dno if they changed that.
The TS doesn't seem to provide for a way to modify return values for the function. For example the following is a common pattern in Go using defer to ensure that errors closing a writeable file are returned:
I don't even bother with `error1`, `error2`, ... `errorN`.
I initialise all pointers to NULL at the top of the function and use `goto cleanup`, which cleans up everything that is not being returned ... because `free(some_ptr)` where `some_ptr` is NULL is perfectly legal.
Yes, the proposal is tailored so that other than simple syntax support no new semantics need to be implemented within GCC to support defer, though clang will need to finally add support for nested functions--in spirit if not the literal GCC extension.[1] The proposal also gives consideration to MSVC's try/finally to minimize the amount of effort required there to support defer.
[1] Because defer takes a block, not a simple statement. And deferred blocks can be defined recursively--i.e. defer within a defer block.
Go's "defer" is reasonably clean because Go is garbage-collected. So you don't have to worry about something being deleted before a queued "defer" runs. That's well-behaved. This is going to be full of ugly, non-obvious problems.
Interestingly, it's not really "defer" in the Go sense. It's "finally", in the try/finally sense of C++, using Go-type "defer" syntax". This mostly matters for scope and ownership issues. If you want to close a file in a defer, and the file is a local variable, you have to be sure that the close precedes the end of block de-allocation. Most of the discussion in the article revolves around how to make such problems behave halfway decently.
"defer" happens invisibly, in the background. That's contrary to the basic simplicity of C, where almost nothing happens invisibly.
Ever since I started working with Zig, I came to realization that its errdefer is even more useful than defer itself. But you can't implement errdefer in C, since there is no standard/disambiguous way of returning errors.
I always thought golang's defer was a readability nightmare because it obfuscates execution order. OP's "basic example" is ... a great example of obfuscation. try/finally doesn't have this problem. It can add indents, but I'd so much rather read a function with 4 indents than 4 defers
Pretty much the only time I use it, is if the act of doing some cleanup might cause a change (like a mutable function in a communication API, or letting go of a reference may interfere with a last operation).
Generally, I find it isn’t necessary. I can usually figure out a way to make it work with standard flow control.
fwlr|11 months ago
> If we flew planes like we write code, we’d have daily crashes, of course, but beyond that, the response to every plane crash would be: “only a bad pilot blames their plane!”
> This doesn’t happen in aviation, because in aviation we have decided, correctly, that human error is an intrinsic and inseparable part of human activity. And so we have built concentric layers of mechanical checks and balances around pilots, to take on part of the load of flying. Because humans are tired, they are burned out, they have limited focus, limited working memory, they are traumatized by writing executable YAML, etc.
> Mechanical processes are independent of the skill of the programmer. Mechanical processes scale, unlike berating people to simply write fewer bugs.
(https://borretti.me/article/introducing-austral#goals)
zombot|11 months ago
It gives me solace to know that I am not alone.
majormajor|11 months ago
smadge|11 months ago
- the language still allows you write the unsafe version even with defer. By your logic fallible humans will continue to write these class of bugs because they can. - adding a whole new flow control construct will introduce a whole new class of bugs. The dog barking example is cool for demonstrating how defer works, but is completely unreadable for what it does, programmers will write code like that because they are allowed to, and unreadable code becomes buggy code. - to make a language safer you should remove the things that make unsafe behavior possible, not add constructs which make safe behavior easier.
Jtsummers|11 months ago
The only reasonable way for defer to behave. Function scoped never made sense to me given the wasted potential. The demonstration with loop and mutex being a good one.
gblargg|11 months ago
chrsig|11 months ago
topspin|11 months ago
Disclaimer: Not a golang hater. Great language. Used it myself on occasion, although I remain a golang neophyte. Put away the sharp objects.
hinkley|11 months ago
majormajor|11 months ago
I think per scope-level is probably better, but honestly still - as a I mention elsewhere - still something that seems fairly limited compared to writing code inside blocks that clean themselves up in the Ruby world. The more we're messing with scope, the more it seems like it would be possible to go all the way to that? The go-style defer appears likely to be simpler from an implementation POV; if we're gonna make it harder let's go all the way!
I know a lot of people hate the nesting of indentation from that, but it makes so many other things harder to screw up.
wruza|11 months ago
Edit: Same for iifes. Everyone uses them, so turn these into proper embedded blocks!
rollulus|11 months ago
dgunay|11 months ago
fuhsnn|11 months ago
[1] https://github.com/fuhsnn/slimcc
Mond_|11 months ago
Glad to see C is evolving and standardizing.
throw-qqqqq|11 months ago
At least this was the case last I did benchmarks of my Go code. Dno if they changed that.
klodolph|11 months ago
aeijdenberg|11 months ago
zyedidia|11 months ago
loeg|11 months ago
lelanthran|11 months ago
I initialise all pointers to NULL at the top of the function and use `goto cleanup`, which cleans up everything that is not being returned ... because `free(some_ptr)` where `some_ptr` is NULL is perfectly legal.
bobmcnamara|11 months ago
ayende|11 months ago
That means you won't forget to call it, and the success flag is an obvious way to ha dle it
unknown|11 months ago
[deleted]
neilv|11 months ago
Why not make the string literals in the code identify their positions in the output, to expose the behavior, rather than obfuscate it?
Then the reader only has to work through the code, to see why it would have that order.
It currently looks like a puzzle intended to be harder for the reader to understand than it needs to be.
hyperhello|11 months ago
gblargg|11 months ago
codr7|11 months ago
https://github.com/codr7/hacktical-c/tree/main/macro
wahern|11 months ago
[1] Because defer takes a block, not a simple statement. And deferred blocks can be defined recursively--i.e. defer within a defer block.
saagarjha|11 months ago
Animats|11 months ago
Go's "defer" is reasonably clean because Go is garbage-collected. So you don't have to worry about something being deleted before a queued "defer" runs. That's well-behaved. This is going to be full of ugly, non-obvious problems.
Interestingly, it's not really "defer" in the Go sense. It's "finally", in the try/finally sense of C++, using Go-type "defer" syntax". This mostly matters for scope and ownership issues. If you want to close a file in a defer, and the file is a local variable, you have to be sure that the close precedes the end of block de-allocation. Most of the discussion in the article revolves around how to make such problems behave halfway decently.
"defer" happens invisibly, in the background. That's contrary to the basic simplicity of C, where almost nothing happens invisibly.
codr7|11 months ago
jayd16|11 months ago
What sense is that? C++ doesn't have finally and the article explicitly calls out how its not like destructors.
unknown|11 months ago
[deleted]
lukaslalinsky|11 months ago
infogulch|11 months ago
unknown|11 months ago
[deleted]
sbrudenell|11 months ago
ChrisMarshallNY|11 months ago
Generally, I find it isn’t necessary. I can usually figure out a way to make it work with standard flow control.
In my case, it’s the Swift language.
Wumpnot|11 months ago
hyperhello|11 months ago
gizmo686|11 months ago
unknown|11 months ago
[deleted]
kats|11 months ago
[deleted]
kats|11 months ago
[deleted]
_kst_|11 months ago
(And I hardly think that analyzing speculating about the motivation for the author's chosen nickname is constructive.)