Different Variable Scoping Paradigms
Some coworkers and I were discussing good/bad practices for the Go programming language yesterday and specifically Go’s Named Result Parameters feature which I really like.
The Catalyst: Named Result Parameters in Go
You can read more about Named Result Parameters in the Go documentation but the TL;DR is that just as you give names to the parameters that your function receives you can also name the returns of your function along with their appropriate types, you define them in the following format:
Some notes from this:
1 - Note how I’m just using the variable without first having to declare it at all, the value is already initialized when the function starts and so I can just use it.
2 - Same with err, the variable is already declared with its appropriate type.
3 - The return for this would be
4, "", nil, note how I never ever explicitly did anything with processedString, an empty string is returned since its the zero value for strings in Go.
I really like this feature as it saves me from intializing values - so there is less boilerplate code - and it documents the function that much more efficiently since I can know everything I care about knowing just by looking at the declaration. Neat.
Now one of my coworkers expressed that he had mixed feelings about it, because while working with if blocks or for loops you could be working with a different variable with the same name, which would make the
return statement send back different data depending on the context it’s being called (inside/outside of an if or for). I panicked a bit when talking about this because I simply didn’t know that if and for loops opened new variable scopes in Go, this is of course also the case in C - and probably a lot if not most of other static languages - and it’s precisely the kind of things that can easily lead to bugs. So I decided to play around a bit with different scoping to learn about things.
Function-wide variable scopes.
I’m mostly used to working with dynamic languages where variables are scoped to the function you initialize them on, you can’t really declare variables in dynamic languages as they are spawned into existence when you assign something to them.
Most of you will be familiar with this and it won’t come as a surprise, but my coworkers are C people and they were as amazed by the fact that you can’t have a variable that is only scoped to an if statement as I was by the fact that that’s a normal thing in static languages.
Which is fairly obvious, right? There is no such thing as variable declaration in certain dynamically typed languages so it makes sense for variable scoping to work this way.
This example on Python (3) shows us a bit more about how variable scoping works on this paradigm.
function we are just going over the same expected behaviour as Ruby,
another_function shows us how defining a new
nested_function will create a new scope for variables, and finally
yet_another_function shows us how we can easily access variables on the parent scope from a nested function.
if/for variable scopes.
In Go and statically typed languages in general you are required to declare your variables before using them, there is some syntactic sugar in Go to make that easier but variables always need to be declared in some way.
What did strike me as odd though is that whenever you use control flow like on an
if statement a new variable scope is opened, you still have access to the function scope but variables declared inside the
if statement don’t make it to the outside, this can become tricky when somebody has the poor judgment of declaring a variable inside an
if scope with the same name as a variable outside of it.
As we see in
testCase1 Go behaves like our previous dynamic examples and according to what intuition would dictate: the
if statement scope has access to the parent scope, we can just use that variable, no fuzz.
testCase2 we see how we can declare a new variable with the same name as the one in the parent scope and use it and the variable on the parent scope is unaffected, this means that there is no way (that I know of) to access the original variable since whatever you try to do with
variable in the if clause will use the newly declared variable instead of the one in the parent scope.
This is not particular to static languages either,
lua for example presents the following behavior when declaring variables.
As a rule of thumb I’d say doing this is a very bad idea since it confuses things, thinking of a new variable name is not that hard, dammit! Maybe it wouldn’t be a big deal with variables like
err that are consistently used for errors but on most other cases I’d say people should just avoid this.
But people might not avoid it, so it could potentially be a big danger point on our Named Return Parameters, since we would have to double check what we are doing whenever we
return from inside any control statement, right?
Well, turns out the Go people are smarter than that. Let’s see what happens when we try to
return in the proposed circumstances:
We get a compiler error, Go code that does this kind of thing will not even be built and the error message variable is shadowed during return will be returned, all is well in Goland.
I am obviously biased by my experience but I find I prefer function-wide scoping for variables. I think this conditional statement scoping is a relic from languages which need much more verbosity to get things done and so it make sense to have a way of not contamining the function scope with short-lived variables, it doesn’t seem necessary anymore with a language like Go but in the end and provided a slight dose of common sense by programmers I don’t think it’s a big deal, just something to be aware of.