More significantly the new variables x and y in the block are Drop'd at the end of the block rather than at the end of the function. This can be significant if:
- Drop does something, like close a file or release a lock, or
- x and y don't have Send and/or Sync, and you have an await point in the function or are doing multi-threaded stuff
This is why you should almost always use std::sync::Mutex rather than tokio::sync::Mutex. std's Mutex isn't Sync/Send, so the compiler will complain if you hold it across an await. Usually you don't want mutex's held across an await.
Can this also affect stack usage? Like if `x` gets dropped before `y` is introduced, can `y` reuse `x`'s stack space (let's assume they are same size/alignment). Or does the compiler already do that if it can see that one is not used after the other is introduced?
There are some situations with tricky lifetime issues that are almost impossible to write without this pattern. Trying to break code out into functions would force you to name all the types (not even possible for closures) or use generics (which can lead to difficulties specifying all required trait bounds), and `drop()` on its own is of no use since it doesn't effect the lexical lifetimes.
That's all fine until later on, probably in some obscure loop, `i_think_this_is_setup` is used without you noticing.
Instead doing something like this tells the reader that it will be used again:
i_think_this_is_setup = even_more_stuff
the_thing = begin
setup_a = some_stuff
setup_b = some_more_stuff
run_setup(setup_a, setup_b, i_think_this_is_setup)
end
I now don't mentally have to keep track of what `setup_a` or `setup_b` are anymore and, since the writer made a conscious effort not to put it in the block, you will take an extra look for it in the outer scope.
Clojure also has the threading macro -> and ->> which are great at converting exactly the same type of code into a stream of modifications instead of breaking out everything into variables. Naming things can be very useful sometimes but sometimes it is entirely gratuitous and distracting to have
let input = read_input();
let trimmed_input = input.trim();
let trimmed_uppercase_input = trimmed_input.uppercase();
...
The extra variable names are almost completely boilerplate and make it also annoying to reorder things.
In Clojure you can do
(-> (read-input) string/trim string/upcase)
And I find that so much more readable and refactorable.
Not mentioned in the article but kinda neat: you can label such a block and break out of it, too! The break takes an argument that becomes the value of the block that is broken out of.
This seems like a great way to group semantically-related statements, reduce variable leakage, and reduce the potential to silently introduce additional dependencies on variables. Seems lighter weight (especially from a cognitive load perspective) than lambdas. Appropriate for when there is a single user of the block -- avoids polluting the namespace with additional functions. Can be easily turned into a separate function once there are multiple users.
This is one of those natural consequences of "everything is an expression" languages that I really like! I like more explicit syntax like Zig's labelled blocks, but any of these are cool.
Try this out, you can actually (technically) assign a variable to `continue` like:
let x = continue;
Funnily enough, one of the few things that are definitely always a statement are `let` statements! Except, you also have `let` expressions, which are technically different, so I guess that's not really a difference at all.
I agree, i started with (scope) blocks in Rust, but keep the habit in Kotlin win the run - scope-function. Since run takes no arguments, it feels like the closest equivalent to Rust scopes (compared to other Korlin scope functions, which also keep their local variables from polluting the rest of the function body).
I typically use closures to do this in other languages, but the syntax is always so cumbersome. You get the "dog balls" that Douglas Crockford always called them:
```
const config = (() => {
const raw_data = ...
...
return compiled;
})()'
const result = config.whatever;
// carry on
return result;
```
Really wish block were expressions in more languages.
The first example given is not at all convincing. Its is clear as the sky that loading the config file should be be a separate function of its own. Coupling sending HTTP requests with it makes no sense.
The second example "erasure of mutability" makes more sense. But this effectively makes it a Rust-specific pattern.
It's essentially an inline function with only 1 client. Can be a preference for inline readability and automatically enforces there are no other clients of the "function".
I have one better: the try block pattern.
https://doc.rust-lang.org/beta/unstable-book/language-featur...
I want that stabilized so bad but it's not been really moving forward.
Now that is pretty cool.
More significantly the new variables x and y in the block are Drop'd at the end of the block rather than at the end of the function. This can be significant if:
- Drop does something, like close a file or release a lock, or
- x and y don't have Send and/or Sync, and you have an await point in the function or are doing multi-threaded stuff
This is why you should almost always use std::sync::Mutex rather than tokio::sync::Mutex. std's Mutex isn't Sync/Send, so the compiler will complain if you hold it across an await. Usually you don't want mutex's held across an await.
Can this also affect stack usage? Like if `x` gets dropped before `y` is introduced, can `y` reuse `x`'s stack space (let's assume they are same size/alignment). Or does the compiler already do that if it can see that one is not used after the other is introduced?
oops: Of course the Mutex is Sync/Send, that's the whole point of a Mutex. It's the std::sync::MutexGuard that's not.
There are some situations with tricky lifetime issues that are almost impossible to write without this pattern. Trying to break code out into functions would force you to name all the types (not even possible for closures) or use generics (which can lead to difficulties specifying all required trait bounds), and `drop()` on its own is of no use since it doesn't effect the lexical lifetimes.
I often employ this pattern in Ruby using `.tap` or a `begin` block.
It barely adds any functionality but it's useful for readability because of the same reasons in the OP.
It helps because I've been bitten by code that did this:
That's all fine until later on, probably in some obscure loop, `i_think_this_is_setup` is used without you noticing.Instead doing something like this tells the reader that it will be used again:
I now don't mentally have to keep track of what `setup_a` or `setup_b` are anymore and, since the writer made a conscious effort not to put it in the block, you will take an extra look for it in the outer scope.Clojure also has the threading macro -> and ->> which are great at converting exactly the same type of code into a stream of modifications instead of breaking out everything into variables. Naming things can be very useful sometimes but sometimes it is entirely gratuitous and distracting to have
let input = read_input(); let trimmed_input = input.trim(); let trimmed_uppercase_input = trimmed_input.uppercase();
...
The extra variable names are almost completely boilerplate and make it also annoying to reorder things.
In Clojure you can do
(-> (read-input) string/trim string/upcase)
And I find that so much more readable and refactorable.
Not mentioned in the article but kinda neat: you can label such a block and break out of it, too! The break takes an argument that becomes the value of the block that is broken out of.
This seems like a great way to group semantically-related statements, reduce variable leakage, and reduce the potential to silently introduce additional dependencies on variables. Seems lighter weight (especially from a cognitive load perspective) than lambdas. Appropriate for when there is a single user of the block -- avoids polluting the namespace with additional functions. Can be easily turned into a separate function once there are multiple users.
This is one of those natural consequences of "everything is an expression" languages that I really like! I like more explicit syntax like Zig's labelled blocks, but any of these are cool.
Try this out, you can actually (technically) assign a variable to `continue` like:
let x = continue;
Funnily enough, one of the few things that are definitely always a statement are `let` statements! Except, you also have `let` expressions, which are technically different, so I guess that's not really a difference at all.
Reminds of Brian Wills OOP rant video from 2016. He advocates exactly for this pattern: https://www.youtube.com/watch?v=QM1iUe6IofM&t=2235s
It's idiomatic in Kotlin as well!
https://kotlinlang.org/docs/scope-functions.html
So many options why oh why. let run with also apply
I agree, i started with (scope) blocks in Rust, but keep the habit in Kotlin win the run - scope-function. Since run takes no arguments, it feels like the closest equivalent to Rust scopes (compared to other Korlin scope functions, which also keep their local variables from polluting the rest of the function body).
I love that this is part of the syntax.
I typically use closures to do this in other languages, but the syntax is always so cumbersome. You get the "dog balls" that Douglas Crockford always called them:
``` const config = (() => { const raw_data = ...
})()'const result = config.whatever;
// carry on
return result; ```
Really wish block were expressions in more languages.
You can also de-mut-ify a variable by simply shadowing it with an immutable version of itself:
let mut data = foo(); data.mutate(); let data = data;
May be preferable for short snippets where adding braces, the yielded expression, and indentation is more noise than it's worth.
Block expression https://doc.rust-lang.org/reference/expressions/block-expr.h...
Also in Kotlin, Scala, and nim.
GCC adds similar syntax as an extension to C: https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html
It's used all throughout the Linux kernel and useful for macros.
The first example given is not at all convincing. Its is clear as the sky that loading the config file should be be a separate function of its own. Coupling sending HTTP requests with it makes no sense.
The second example "erasure of mutability" makes more sense. But this effectively makes it a Rust-specific pattern.
It's essentially an inline function with only 1 client. Can be a preference for inline readability and automatically enforces there are no other clients of the "function".
Obligatory use: it’s a block I guess
Voluntary use: I know this one. It’s a pattern now.