Tips and tricks with implicit return in Nim
1st November 2019 - Language design , Guide , Nim , Programming
One feature in Nim that it shares with several other languages, especially functional ones, is implicit return of values. This is one of those small features that might just seem like a way to avoid writing return
everywhere. But in fact it can be used for much more than that! So what is implicit return? In Nim we have three ways to return a value from a procedure, you have the explicit way with return
, you have the implicit return via result
and then the implicit return via the last statement in the procedure. Consider this simple code:
proc explicit(): string =
return "Typical explicit return"
proc implicitResult(): string =
result = "Implicit return through result"
proc implicitLastStatement(): string =
"Implicit return since it's last statement"
The first procedure here does the typical return
that you’re probably familiar with. The second uses the result
variable that is available in every procedure with a return value. This is used extensively throughout Nim to build return values or just assign the return before the procedure ends. The third way is maybe the most interesting of the three, and the one this article is mainly about. Because most blocks in Nim will implicitly return the result of their last statement if it matches the return type. If you change the return type of the above statement it will tell you that you need to discard
the string. In this case the string is a literal, but it could just as well be a call to another procedure that returned a string. As you might know unused return variables need to be discarded in Nim, but if it’s the last statement in a procedure it will be returned instead.
So that’s cool, but so what?
As I mentioned above this might seem like a tiny insignificant feature that just saves us from typing a return
or result =
in our procedures. But it’s so much more than that! Because it’s not only procedures that have this ability it’s most blocks. Take for example the classic ternary in C:
int x = somecall() ? 100 : 200;
This will return 100 if somecall
returns something that is evaluated to true, and 200 otherwise. It’s a nice way to assign a variable in a single line based on some condition. But Nim doesn’t have ternaries, what you do in Nim instead is to simply use a regular if statement:
let x = if somecall(): 100 else: 200
This does exactly the same thing as above, and it works because if statements will implicitly return the result of the last statement, exactly like procedures. Of course it’s not just true for the simple if/else case, but also extends to elif
:
let x = if somecall(): 100
elif someothercall(): 200
else: 300
As you can see here you can of course also split these into multiple lines, just like a regular if
statement. Such if statements must of course be guaranteed to return something for this to work. So if you drop the else
case, or if one of the branches doesn’t return something you will get an error about having to discard the other values. And this brings us to the first cool trick. Imagine you have a value that you need to assign based on some complex conditions:
var y = 300
if somecall() > 100:
dosomething(@["item1", "item2"])
y = 100
elif someothercall() < thirdcall():
anotheraction("Hello world")
y = 200
This is probably the pattern you would use in another language. But it’s easy to imagine refactoring this to add another branch and forgetting to set y
, or just delete one of the existing ones by accident. With the implicit returns though we can let the compiler check that we actually set y
:
let y =
if somecall() > 100:
dosomething(@["item1", "item2"])
100
elif someothercall() < thirdcall():
anotheraction("Hello world")
200
else:
300
This guarantees that y
gets a value, and since these are just completely normal if
statements you can of course drop any amount of logic in them. And even let the last statement in the block be the return value. Including another if
block of course. But the fun isn’t over yet. You might’ve noticed that the second example uses a let
assignment instead of a var
. That’s right, using this method of initialisation means that the assignment can be immutable! Just a little extra safety in case you try to do something dumb by accident later on.
But wait, there’s more!
As I mentioned above this works for all kinds of blocks. So you can use case
statements:
let z = case somecall():
of int.low..1000:
"Low"
of 1001..2000:
"Medium"
else:
"High"
Or how about default values in the case of an exception by using a try
statement?
import strutils
let a = try: parseFloat("100.0") except ValueError: 0.0
No more having to write four lines of code to have proper error handling just to use a default value. Another cool thing is to solve one of the two hard problems in computer science:
There are only two hard things in Computer Science: cache invalidation and naming things.
– Phil Karlton
Since Nim also has a simple block
statement we can create blocks of arbitrary code. And since everything has implicit returns, we can use them to make our own mini-scopes when assigning variables:
let myAccumulation =
block:
var x = ""
for i in 1..5:
x &= $i & " "
x
The variable x
here is not visible outside of that block, which means we can’t use it by accident instead of myAccumulation
. And since it only exists in this scope we can re-use the name, so we can easily create small temporary variables that only live in the assignment scope of another value, and are then promptly forgotten about, and of course then just call them tmp
or something equally silly.
Conclusion
The question of how to do default values for parseFloat
came up on IRC, which is what prompted me to write this little article. It showcases how this easily overlooked feature can actually be used for not only real-world issues, but also how it can improve our code in interesting ways and help us avoid mistakes. Hopefully this will be useful to someone else as well.