Our friend Scope
Thursday, 17 February 2011
Scope is one of those things that is absolutely essential to understand as a programmer. If you don’t understand how a language handles scope, you are doomed to continue making difficult-to-understand mistakes and poor code design.
Scheme is a good platform for exploring how scope works for beginners, and the language features that it has distilled to the minimum form the basis of 99% of the cases you encounter from day to day.
Let’s look at one pattern that you’ll find in every jQuery plugin:
[javascript]
(function ($) {
// some jQuery code
})(jQuery)
[/javascript]
What does it do? It temporarily binds the variable $ to jQuery only inside the function body. Clever trick? Well… no, because this is familiar to anybody who understands Scheme. Let’s look at the Scheme equivalent:
[scheme]
((lambda ($) some-code) jQuery)
[/scheme]
Oh, that looks familiar. It is just the elementary equivalency for let
:
[scheme]
(let (($ jQuery))
code)
[/scheme]
Side effects
Here’s another JavaScript, jQuery example:
[javascript]
var i
for(i = 0; i < 5; i++) {
$("#container-div").append(
$("<button type=’button’>Click me!</button>")
.click(function() {
alert(i)
})
)
}
[/javascript]
Before you click, guess what each button will display!
container-div:
If you guessed correctly, give yourself a pat on the back. The simple explanation here is that you only ever create one variable i
, so you can’t have more than one value for i
.
Let’s fix that by introducing a new variable k
inside the loop, so that each alert gets bound to a new k
.
[javascript]
var i
for(i = 0; i < 5; i++) {
var k = i
$("#container-div").append(
$("<button type=’button’>Click me!</button>")
.click(function() {
alert(k)
})
)
}
[/javascript]
Before you click, guess what each button will display!
container-div2:
Translating Javascript to Scheme
We might better understand this in Scheme.
Begin by moving all var
declarations to the top of their function body while keeping their references intact:
[javascript]
var i
var k
for(i = 0; i < 5; i++) {
k = i
$("#container-div2").append(
$("<button type=’button’>Guess again!</button>")
.click(function() {
alert(k)
})
)
}
[/javascript]
The var declarations at the top are just Scheme internal defines that we wrap in a blank let:
[scheme]
(let ()
(define i (void))
(define k (void))
;; for loop…
)
[/scheme]
The internal defines expand into a let statement, and the wrapper let is no longer needed:
[scheme]
(let ((i (void))
(k (void)))
;; for loop …
)
[/scheme]
We’re just going to unroll this loop and convert the jQuery stuff into some pseudo-code. It would be wrong to convert this loop into a recursive program.
[scheme]
(let ((i (void))
(k (void)))
(set! i 0)
(set! k i)
(append! container-div2
(make-button "Guess again!"
(lambda ()
(alert k))))
(set! i 1)
(set! k i)
(append! container-div2
(make-button "Guess again!"
(lambda ()
(alert k))))
(set! i 2)
(set! k i)
(append! container-div2
(make-button "Guess again!"
(lambda ()
(alert k))))
…
)
[/scheme]
You see that there is only ever one i
and one k
, and that we are simply mutating their values. Furthermore, lexical scope tells us that the free vars k
in each lambda capture a that r
, but doesn’t make a copy. The use of set!
should also ring warning bells, especially with its “!” suffix.
Let’s fix that with a let:
[scheme]
(append! container-div2
(make-button "Guess again!"
(let ((k k))
(lambda ()
(alert k)))))
[/scheme]
What this does is extend the environment with a new k
(that has the value of the old k
) only for the body of that lambda. This way, we make sure we capture the value of k as it was at that point in time.
Javascript doesn’t have let
, but it does have first-class closures, so let’s translate this let into lambdas:
[scheme]
(append! container-div2
(make-button "Guess again!"
((lambda (k)
(lambda ()
(alert k))) k)))
[/scheme]
and back into Javascript:
[javascript]
$("container-div2").append(
$("<button type=’button’></button>")
.click((function (k) {
return function () {
alert(k)
}
})(k))
)
[/javascript]
and it does work as expected:
container-div3:
A moral?
Don’t put var declarations inside loops. They’re deceptive. Also, don’t trust the language to automatically generate new instances of things for you. Make it explicit.