Files
davideisinger.com/static/archive/yehudakatz-com-sacizu.txt
2024-01-31 10:56:46 -05:00

358 lines
11 KiB
Plaintext

[1] Katz Got Your Tongue
• [3]Home
• [4]About
• [5]Projects
• [6]Talks
• [7]Podcasts
• [8]Schedule
Login Subscribe
Jan 9, 2012 6 min read
JavaScript Needs Blocks
While reading Hacker News posts about JavaScript, I often come across the
misconception that Ruby's blocks are essentially equivalent to JavaScript's
"first class functions". Because the ability to pass functions around,
especially when you can create them anonymously, is extremely powerful, the
fact that both JavaScript and Ruby have a mechanism to do so makes it natural
to assume equivalence.
In fact, when people talk about why Ruby's blocks are different from Python's
functions, they usually talk about anonymity, something that Ruby and
JavaScript share, but Python does not have. At first glance, a Ruby block is an
"anonymous function" (or colloquially, a "closure") just as a JavaScript
function is one.
This impression, which I admittedly shared in my early days as a Ruby/
JavaScript developer, misses an important subtlety that turns out to have large
implications. This subtlety is often referred to as "Tennent's Correspondence
Principle". In short, Tennent's Correspondence Principle says:
"For a given expression expr, lambda expr should be equivalent."
This is also known as the principle of abstraction, because it means that it is
easy to refactor common code into methods that take a block. For instance,
consider the common case of file resource management. Imagine that the block
form of File.open didn't exist in Ruby, and you saw a lot of the following in
your code:
begin
f = File.open(filename, "r")
# do something with f
ensure
f.close
end
In general, when you see some code that has the same beginning and end, but a
different middle, it is natural to refactor it into a method that takes a
block. You would write a method like this:
def read_file(filename)
f = File.open(filename, "r")
yield f
ensure
f.close
end
And you'd refactor instances of the pattern in your code with:
read_file(filename) do |f|
# do something with f
end
In order for this strategy to work, it's important that the code inside the
block look the same after refactoring as before. We can restate the
correspondence principle in this case as:
```ruby # do something with f ```
should be equivalent to:
do
# do something with
end
At first glance, it looks like this is true in Ruby and JavaScript. For
instance, let's say that what you're doing with the file is printing its mtime.
You can easily refactor the equivalent in JavaScript:
try {
// imaginary JS file API
var f = File.open(filename, "r");
sys.print(f.mtime);
} finally {
f.close();
}
Into this:
read_file(function(f) {
sys.print(f.mtime);
});
In fact, cases like this, which are in fact quite elegant, give people the
mistaken impression that Ruby and JavaScript have a roughly equivalent ability
to refactor common functionality into anonymous functions.
However, consider a slightly more complicated example, first in Ruby. We'll
write a simple class that calculates a File's mtime and retrieves its body:
class FileInfo
def initialize(filename)
@name = filename
end
# calculate the File's +mtime+
def mtime
f = File.open(@name, "r")
mtime = mtime_for(f)
return "too old" if mtime < (Time.now - 1000)
puts "recent!"
mtime
ensure
f.close
end
# retrieve that file's +body+
def body
f = File.open(@name, "r")
f.read
ensure
f.close
end
# a helper method to retrieve the mtime of a file
def mtime_for(f)
File.mtime(f)
end
end
We can easily refactor this code using blocks:
class FileInfo
def initialize(filename)
@name = filename
end
# refactor the common file management code into a method
# that takes a block
def mtime
with_file do |f|
mtime = mtime_for(f)
return "too old" if mtime < (Time.now - 1000)
puts "recent!"
mtime
end
end
def body
with_file { |f| f.read }
end
def mtime_for(f)
File.mtime(f)
end
private
# this method opens a file, calls a block with it, and
# ensures that the file is closed once the block has
# finished executing.
def with_file
f = File.open(@name, "r")
yield f
ensure
f.close
end
end
Again, the important thing to note here is that we could move the code into a
block without changing it. Unfortunately, this same case does not work in
JavaScript. Let's first write the equivalent FileInfo class in JavaScript.
// constructor for the FileInfo class
FileInfo = function(filename) {
this.name = filename;
};
FileInfo.prototype = {
// retrieve the file's mtime
mtime: function() {
try {
var f = File.open(this.name, "r");
var mtime = this.mtimeFor(f);
if (mtime < new Date() - 1000) {
return "too old";
}
sys.print(mtime);
} finally {
f.close();
}
},
// retrieve the file's body
body: function() {
try {
var f = File.open(this.name, "r");
return f.read();
} finally {
f.close();
}
},
// a helper method to retrieve the mtime of a file
mtimeFor: function(f) {
return File.mtime(f);
}
};
If we try to convert the repeated code into a method that takes a function, the
mtime method will look something like:
function() {
// refactor the common file management code into a method
// that takes a block
this.withFile(function(f) {
var mtime = this.mtimeFor(f);
if (mtime < new Date() - 1000) {
return "too old";
}
sys.print(mtime);
});
}
There are two very common problems here. First, this has changed contexts. We
can fix this by allowing a binding as a second parameter, but it means that we
need to make sure that every time we refactor to a lambda we make sure to
accept a binding parameter and pass it in. The var self = this pattern emerged
in JavaScript primarily because of the lack of correspondence.
This is annoying, but not deadly. More problematic is the fact that return has
changed meaning. Instead of returning from the outer function, it returns from
the inner one.
This is the right time for JavaScript lovers (and I write this as a sometimes
JavaScript lover myself) to argue that return behaves exactly as intended, and
this behavior is simpler and more elegant than the Ruby behavior. That may be
true, but it doesn't alter the fact that this behavior breaks the
correspondence principle, with very real consequences.
Instead of effortlessly refactoring code with the same start and end into a
function taking a function, JavaScript library authors need to consider the
fact that consumers of their APIs will often need to perform some gymnastics
when dealing with nested functions. In my experience as an author and consumer
of JavaScript libraries, this leads to many cases where it's just too much
bother to provide a nice block-based API.
In order to have a language with return (and possibly super and other similar
keywords) that satisfies the correspondence principle, the language must, like
Ruby and Smalltalk before it, have a function lambda and a block lambda.
Keywords like return always return from the function lambda, even inside of
block lambdas nested inside. At first glance, this appears a bit inelegant, and
language partisans often accuse Ruby of unnecessarily having two types of
"callables", in my experience as an author of large libraries in both Ruby and
JavaScript, it results in more elegant abstractions in the end.
Iterators and Callbacks
It's worth noting that block lambdas only make sense for functions that take
functions and invoke them immediately. In this context, keywords like return,
super and Ruby's yield make sense. These cases include iterators, mutex
synchronization and resource management (like the block form of File.open).
In contrast, when functions are used as callbacks, those keywords no longer
make sense. What does it mean to return from a function that has already
returned? In these cases, typically involving callbacks, function lambdas make
a lot of sense. In my view, this explains why JavaScript feels so elegant for
evented code that involves a lot of callbacks, but somewhat clunky for the
iterator case, and Ruby feels so elegant for the iterator case and somewhat
more clunky for the evented case. In Ruby's case, (again in my opinion), this
clunkiness is more from the massively pervasive use of blocks for synchronous
code than a real deficiency in its structures.
Because of these concerns, the ECMA working group responsible for ECMAScript,
TC39, [12]is considering adding block lambdas to the language. This would mean
that the above example could be refactored to:
FileInfo = function(name) {
this.name = name;
};
FileInfo.prototype = {
mtime: function() {
// use the proposed block syntax, `{ |args| }`.
this.withFile { |f|
// in block lambdas, +this+ is unchanged
var mtime = this.mtimeFor(f);
if (mtime < new Date() - 1000) {
// block lambdas return from their nearest function
return "too old";
}
sys.print(mtime);
}
},
body: function() {
this.withFile { |f| f.read(); }
},
mtimeFor: function(f) {
return File.mtime(f);
},
withFile: function(block) {
try {
var f = File.open(this.name, "r");
block(f);
} finally {
f.close();
}
}
};
Note that a parallel proposal, which replaces function-scoped var with
block-scoped let, will almost certainly be accepted by TC39, which would
slightly, but not substantively, change this example. Also note block lambdas
automatically return their last statement.
Our experience with Smalltalk and Ruby show that people do not need to
understand the SCARY correspondence principle for a language that satisfies it
to yield the desired results. I love the fact that the concept of "iterator" is
not built into the language, but is instead a consequence of natural block
semantics. This gives Ruby a rich, broadly useful set of built-in iterators,
and language users commonly build custom ones. As a JavaScript practitioner, I
often run into situations where using a for loop is significantly more
straight-forward than using forEach, always because of the lack of
correspondence between the code inside a built-in for loop and the code inside
the function passed to forEach.
For the reasons described above, I strongly approve of [13]the block lambda
proposal and hope it is adopted.
[14]
Published by:
[15] Yehuda Katz
[16]
Katz Got Your Tongue © 2024
[17]Powered by Ghost
[pixel]
References:
[1] https://yehudakatz.com/
[3] http://www.yehudakatz.com/
[4] https://yehudakatz.com/about/
[5] https://yehudakatz.com/projects/
[6] https://yehudakatz.com/talks/
[7] https://yehudakatz.com/podcasts/
[8] https://yehudakatz.com/schedule/
[12] http://wiki.ecmascript.org/doku.php?id=strawman%3Ablock_lambda_revival&ref=yehudakatz.com
[13] http://wiki.ecmascript.org/doku.php?id=strawman%3Ablock_lambda_revival&ref=yehudakatz.com
[14] https://yehudakatz.com/2011/12/12/amber-js-formerly-sproutcore-2-0-is-now-ember-js/
[15] https://yehudakatz.com/author/wycats/
[16] https://yehudakatz.com/2012/04/13/tokaido-my-hopes-and-dreams/
[17] https://ghost.org/