Of course, blocks-are-expressions is necessary if you want to return the result of your computations from the block and store it in a variable outside the block. (You can of course declare the storage variable outside the block and assign to it inside, but that's less nice.)
GNU C even has an extension called "statement expressions" where blocks can return values; the syntax looks like this:
int foo = ({
int bar = 4 * 3;
bar;
});
Clang implements it in addition to GCC, as do a few other compilers. (Notably, IIRC, MSVC does not.) void someProc() {
char buffer1[256];
{
char buffer2[256];
// string ops
}
// we still have 512 bytes on the stack whereas it would only be
// 256 if we used a non-inline function instead of a block
someDeepCall();
}
I'm not sure what Rust or Go do in cases like this. result := func() string {
helperVar1 := //...
helperVar2 := //...
return helperVar1 + helperVar2
}()
So it’s basically an anonymous function that is invoked right away. You could achieve the same scope separation by pulling it out as named function, but sometimes I like it better to keep things closer together. for _, fileName := range fileNames {
f, err := os.Open(fileName)
if err != nil {
return err
}
defer f.Close()
doSomething(f)
}
... because I might run out of quota for open file handles. I want the defer to trigger at the end of the loop rather than at the end of the function, so I'll often put a closure in the loop body: for _, fileName := range fileNames {
if err := func() error {
f, err := os.Open(fileName)
if err != nil {
return err
}
defer f.Close()
doSomething()
return nil
}(); err != nil {
return err
}
}
That said, I don't like the ergonomics and if I'm doing a lot of file things, I'll write a `func withFile(fileName string, callback func(*os.File) error) error` function which often composes more nicely. var pkgGlobal = func() string {
...
}()
Much better than: var pkgGlobal string
func init() {
pkgGlobal = ...
}ETA: except for go's `defer`, and off the top of my head I don't actually know if Go is obliged to run the defer immediately upon exiting the block or can choose to run it at some other point in the function.
I guess it depends on what you mean by "context" but the spec is very clear that a block creates scope, and the end removes scope.
https://go.dev/ref/spec#Declarations_and_scope
> The scope of a constant or variable identifier declared inside a function begins at the end of the ConstSpec or VarSpec (ShortVarDecl for short variable declarations) and ends at the end of the innermost containing block.
I rarely use a free-standing {block} in actual code. I think it's because if something is worthy enough to be logically grouped into a {block}, then it is probably worthwhile to pull it out into its own function, method or lambda expression.
If you aren't already it's perhaps better to identify an object that's being locked, which you can have the Mutex wrap. So e.g. you could have a Mutex<Goose>, and then functions, even methods can take a reference to a Goose to ensure you can't call them by mistake without locking the Mutex - as you otherwise don't have a Goose. If the Goose doesn't need any actual data this will be free at runtime, the compiler type check ensures you took the lock as needed but since it's a Zero Size Type no machine code is emitted to deal with a Goose variable.
Probably your application has a better name for what is being protected than Goose, that's just an example, but having some object that is locked can help ensure you get the locking right and that your mental model of what is being "locked" is coherent.
Of course sometimes there really is no specific thing being locked, even an imaginary one, it's just lock #4 or whatever but in my experience that's rare.
I wanted to point out how q/kdb handles this, because I think it's quite nice.
In kdb, blocks are how you define functions.
{x+1};
That is a function which takes your argument, adds one to it and returns it. (x is the default name for the function argument, another lovely piece of design).If you want to have a named function, just assign it to a variable:
my_inc: {x+1};
Now you can call my_inc(1) and get back 2.Light, consistent and reusable language design, very nice. No need to have two ways of defining functions (e.g. the needless separation of def and lambdas in Python).
The upshot of this is that you get these block constructs for free:
myvar_a: 1;
myvar_b: 2;
my_top_level_var: {
//less important work
}[];
(The one downside is that you need to call the function with the [] brackets)Can you give multiple named variables? How would you write a function like this?
const f = (a,b,c) => { return a * b + c }
Also, is this just q/kdb, or does this also apply to K?You can provide up to 8 named inputs in a function definition.
q example for running sum of 8 inputs:
q)f:{[a;b;c;d;e;f;g;h]sums a,b,c,d,e,f,g,h}
same in K4: q)\
f:{[a;b;c;d;e;f;g;h]+\a,b,c,d,e,f,g,h} f:{z+y*x}
(k is evaluated strictly right to left - another design decision that I quite like -, so I had to move the variables around in the expression)If you wish, you can provide your own variable names as follows:
f:{[a;b;c]c+b*a}
This works in k and q (q is largely k with some nice-to-have functions defined on top). const a = 5;
console.log(a)
{
const a = 10;
console.log(a)
}
console.log(a)
5
10
5
What we are missing from say Rust in js is the blocks being expressions, though there is a proposal ("do expressions") to allow this: const a = do {
if (b) { 5 } else { 10 }
};
https://github.com/tc39/proposal-do-expressionsThe catch with JS is that variable scoping rules are different when you use 'var', if you use 'let' and 'const' the scoping rules work like most programmers would expect for block statements.
In C++ you can really (ab)use it to do things like scoped mutex locks and "stopwatches" that start a timer on construction and print the elapsed time on destruction.
Some people find it a bit bizarre though, to each his/her own I guess.
To be used sparingly, but very useful when it applies e.g. in the precise capture clause pattern, or for non-trivial object initialisation (as Rust doesn't have many literals or literal-ish macros).
if result, err := something(); err == nil {
if result.RowsAffected() == 0 {
return nil
}
} else if err != nil {
return err
}It can be great though as an intermediate step to extracting functions.
I don't use it often, but when I do, I find it convenient.
The problem is that it doesn’t stand out visually, and it’s uncommon, so less-experienced team members would have difficulty comprehending what’s going on. In the end we just opted to use two different variables for the two types of error.
local foo
do
local bar = 42
-- Same as `foo = function() ... end`, so this sets the local foo variable.
function foo()
return bar
end
end
Because Lua has no significant whitespace, it can be made to look like some kind of specific syntax: local foo do
local bar = 42
function foo()
return bar
end
end
Though I think this is too clever, so I like to insert a semi-colon to make it clear what is happening: local foo; do
local bar = 42
function foo()
return bar
end
end'asdf:
{
do_tasks();
if condition {
break 'asdf
}
do_more_tasks();
// ...
}tasks_after_jump();
However, try to compile this with Rust 1.0 (which you can get by running `rustup update 1.0.0` and then using `cargo +1.0.0 run` or `rustc +1.0.0`) and you'll get an error saying that you can't push another element onto the vector due to it already being borrowed by the slice. This is because the borrow checker previously assumed that any borrow would remain in use for the remainder of the scope. The "fix" to this was to manually put in a block to "end" the borrow early. However, a lot of work was done to allow the borrow checker to be more sophisticated, and in Rust 1.31 (near the end of 2018: https://blog.rust-lang.org/2018/12/06/Rust-1.31-and-rust-201...), the work allowing the compiler to recognize that a borrow was no longer used before the end of a scope and therefore would allow subsequent borrows that it previously would have considered conflicting.
All that being said, there's still a useful feature of blocks in Rust that I don't see mentioned in this blog post: blocks in Rust are actually expressions! By default, the last value in a Rust block will be yielded, but you can also use the `break` keyword earlier (similar to how the return value of a Rust function will be the last value, but you can also explicitly `return` earlier). As an added bonus, this also works for `loop` blocks; since they will only ever terminate if `break` is explicitly invoked (unlike `while` or `for`), whatever value is specified will be yielded from the loop.