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
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
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
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
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
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
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.
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.