Fun and surprises with closures, references, and inner functions
Learn about closures while exploring the funky behavior of variables captured in closures in 3 common languages
What happens if you try to reassign a hashmap1 in an inner function, then modify the hashmap with that function’s return value? It might surprise you to know that it depends on the language. Turns out you’ll get a different result in JavaScript, Ruby, and Python!
That means given these 3 virtually identical code blocks below, 3 different things are going on:
Above we have the same code in each language. In English here’s what it does:
Define a hashmap in a function named
outer
Define an
inner
function that will set the empty hashmap to a new hashmap withmy_key
as 9. Theninner
returns 7Call
inner
and setmy_key
to the return valueinner
Print
my_key
Feel free to take a second took make sure you understand what’s going on in the code in the language you’re most comfortable with. Then let’s dive in to find out why this sequence gives us 3 different results in 3 different languages. We’ll review all three closely to see what each prints and why.
JavaScript
Let’s get started with JavaScript. Here’s the same code we saw above.
function outer() {
let my_obj = {};
inner = () => {
my_obj = {my_key: 9};
return 7;
}
my_obj['my_key'] = inner();
console.log(my_obj.my_key);
}
outer()
So what does JavaScript do? Drumroll please… it prints 9.
Wait what? Why does it print my_obj['my_key']
as 9 when the last thing we clearly did was assign 7 to my_obj['my_key']
? Feel free to copy and paste the above code into the JavaScript console on this very page to test it out if you don’t believe me!
It turns out that we assigned 7 to an old version of my_obj
, then we reassigned my_obj
in inner
(to a totally new object) to have 9, and the new version with 9 is what we’re printing out. The old version that gets 7 is no longer referenced.
We can see this more clearly if we save that old version and then print it. You can see the old version prints 7, but the new version has 9.
function outer() {
let my_obj = {};
let my_old_obj = my_obj;
function inner() {
console.log(my_obj); // prints {}
my_obj = {my_key: 9};
return 7;
}
my_obj['my_key'] = inner();
console.log(my_old_obj['my_key']); // prints 7
console.log(my_obj['my_key']); // prints 9
}
outer()
Here’s what’s going on: a closure is capturing my_obj
. A closure is a function that you can save and pass around and captures references to the variables in the environment at the time the function is defined. That means that inside inner
, we have access to my_obj
from outer
. When we reassign my_obj
inside of inner
, we’re reassigning the actual my_obj
we created in outer
.
The tricky part is that JavaScript is using a reference to the original my_obj
to write inner
’s return value into. So we execute inner
which changes my_obj
, but we’re updating my_old_obj
instead.
So to summarize, JavaScript reassigns your object, but it retains a reference to the original object. Then the original object disappears, leaving you with only the one you created in inner
.
Ruby
Now let’s take a look at Ruby:
def outer
my_hash = {}
def inner
my_hash = {:my_key => 9}
7
end
my_hash[:my_key] = inner
puts my_hash[:my_key] # prints 7
end
outer
It looks just like the JavaScript code, but turns out this prints 7!
What’s going on here? We have an inner function, just like in JavaScript, but we don’t have a closure. That means we’re not capturing variables from the environment. It turns out my_hash
is a totally new local variable in inner
. You can especially see that if you try to print out my_hash
in inner
:
def outer
my_hash = {}
def inner
puts my_hash # errors out!
7
end
my_hash[:my_key] = inner
puts my_hash[:my_key]
end
outer
This actually errors out with undefined local variable or method `my_hash' for main:Object.
The variable doesn’t exist because it hasn’t been created in inner
and we don’t have access to anything in outer
.
We can actually create a closure in Ruby, but not like this. We have to use a lambda. This code below creates a closure and actually prints 9, just like our JavaScript code:
def outer
my_hash = {}
inner = lambda do
my_hash = {:my_key => 9} # overwrites my_hash
7
end
my_hash[:my_key] = inner.call # overwrites my_hash[:my_key] with 7
puts my_hash[:my_key] # prints 9
end
outer
Python
Finally let’s take a look at Python. Python prints 7.
def outer():
my_dict = {}
def inner():
my_dict = {'my_key': 9}
return 7
my_dict['my_key'] = inner()
print(my_dict['my_key'])
outer()
So it seems like we’ve got the same situation as we do in Ruby. Probably we don’t have a closure, so if we try to print, we’ll get an error, right?
Wrong! In Python we can print out the variable:
def outer():
my_dict = {}
def inner():
print(my_dict) # prints {}, doesn't error!
return 7
my_dict['my_key'] = inner()
print(my_dict['my_key'])
outer()
So what’s going on? Is it a closure or not? Turns out in Python it’s a closure that lets you read, but not write the captured variable. If we try read and write, we get an error:
def outer():
my_dict = {}
def inner():
print(my_dict) # errors out!
my_dict = {'my_key': 9}
return 7
my_dict['my_key'] = inner()
print(my_dict['my_key'])
outer()
So we can read the outer variable, we can write a new variable, but we can’t do both at once!
There is a workaround though in the form of the Python nonlocal
keyword. If we explicitly say that the variable is not supposed to be local and instead it’s supposed to reference the outer my_dict
using this nonlocal
keyword, then we’ll be actually able to both print and read my_dict
.
def outer():
my_dict = {}
def inner():
nonlocal my_dict
print(my_dict) # works this time!
my_dict = {'my_key': 9}
return 7
my_dict['my_key'] = inner()
print(my_dict['my_key'])
outer() # prints 7
References, closures, and inner functions
Don’t expect reference reassignment in closures to behave consistently from language to language. It’s best to be careful when relying on closures since they can often have unintuitive behavior in edge cases. You don’t want to confuse other developers (or yourself) in your codebase!
Which behavior do you think is best? If you were designing a programming language, which would you choose?
object in JavaScript, hash in Ruby, dictionary in Python