I recently finished reading Andreas's Mastering Nim book, and I really enjoyed seeing all the areas where block-like syntax is used. It's really interesting to see a body of code passed into a template or macro that way.
That being said, I was curious what use cases there are for the literal block statement. It seems like in most cases where you could use block, you'd be better off creating a new proc and throwing the code there.
I use if for tests quite often, so that i can copy and paste without change var/proc names:
block:
const tt = "{%if false%}simple{%endif%}"
check evaluateTemplateStr(tt) == tmpls(tt)
block:
const tt = "{%if 1 == 1%}{%if true%}simple{%endif%}{%endif%}"
check evaluateTemplateStr(tt) == tmpls(tt)
The block statement can carry a label which can be used in a break statement like
block outer :
... within some nested loops
break outer
It seems like in most cases where you could use block, you'd be better off creating a new proc and throwing the code there.
Not really. It's often suggested to only separate a routine into a function when it's used at least twice, with some putting this number even higher. A well indented block not only creates a scope but also has the access to the environment without explicit passing and doesn't require a context switch on the part of a code reader. Using blocks lets you use expressions (contrary to statements) more, frees your code from one-use temporary variables and makes the lifetimes of variables follow the code logic closely. All wins in my book. You just can't go wrong here, and this is coming from the guy who tends to have tons of really short functions.
Some examples from the top of my head:
var (foo, bar) = block:
let t = readFile("input.txt").split("\n\n") # no point of letting `t` to linger to the end of the proc
(frobnicate(t[0]), t[1])
import zero_functional
# ...
var
mi = int.high()
ma = 0
let xorred = input --> map(frobnicate).fold(0, block: # forcing side-effects into a `fold`
mi = min(mi, it)
ma = max(ma, it)
a xor it
)
Thanks, these all help. I definitely have always leaned towards really short functions for easy legibility.
What I hadn't considered is you could combine @Zoom and @HJarausch's comments to neatly document and scope sections of logic which is a pretty cool.
Great thoughtful question. I second the general usage of block above. I also wanted to add that defining templates inside a proc can be useful for legibility as well without needing to define arguments and/or types to a proc. It can be nice to reduce code de-duplication too. Something like this:
proc computeSomething(nodes: seq[SomeNode]) =
template columns(n: untyped): untyped = n.someItem.field.columns
for i, node in nodes:
doSomething(node.columns[I])
doSomethingElse(node.columns[I])
...
A block act just like a lambda function (an anonymous proc ?) but with side effects ?
One syntax caveat is the increase of the indentation level. With if, while, for statements and blocks in between, the code gets quickly too much indented.
Your examples are great, but I am unsure about the second one. Isn't it bad style to "force" side-effects ? You would get a runtime error if you applied your fold on an immutable sequence (a sequence of const/let variables), isn't it ?
For the first example, what is the advantage over a proc exactly ? With a proc you are forced to explain what you do by naming the function. I explicitly gave the result to the special variable, but we are not forced to. I have added generics for type restriction of the result, that is too much for this example.
proc getFirstTwoCharacters[T, U](nameFile: string): tuple[T, U] =
let t = readFile(nameFile).split("\n\n") # no point of letting `t` to linger to the end of the proc
result = (frobnicate(t[0]), t[1])
var (foo, bar) = getFirstTwoCharacters[typeof(frobnicate('a')), char]("input.txt")
blocks are also useful for some horrible things. for example:
var x = acquire_hounds()
block:
defer: release_hounds(x)
.. stuff go here ..
this seems less interesting at first until you realize they can go in to templates or macros. which means you can use templates to create critical sections that will release some resource when the body ends for any reason. so you can build your own python context managers.
or something my math lib does which is amazing and horrifying at the same time:
template broadcasted*[T](v: Vec3[T]; op: untyped): untyped =
## Returns a vector where `op` has been called on each element.
var x, y, z {.noinit.}: T
block:
let it {.inject.} = v.x
x = op
block:
let it {.inject.} = v.y
y = op
block:
let it {.inject.} = v.z
z = op
vec(x, y, z)
which is a template that runs the same body 3 times, operating on a value called it, but actually works on different definitions of it at the same time.
You would get a runtime error if you applied your fold on an immutable sequence (a sequence of const/let variables), isn't it?
No, there's only reading the input and no mutation happening.
Isn't it bad style to "force" side-effects?
I should have explained myself more clearly. This code bit is certainly not the best style, but if I remember correctly, it's a compromise. The "proper" way to write this would be this:
let (xorred, mi, ma) = [1,2,3] --> fold((x: 0, min: int.high(), max: 0),
(a.x xor it, min(a.min, it), max(a.max, it))
)
The previous version is different:
For the first example, what is the advantage over a proc exactly? With a proc you are forced to explain what you do by naming the function.
It's one line and one function call shorter. You're supposed to explain what you're doing by naming foo, bar and frobnicate properly. This is a question of preference, of course, but in this case two-line function seems like an excess. Anyway, this is just an example of limiting the lifetime of a variable.