davideisinger.com

My personal website
Log | Files | Refs | README

yehudakatz-com-sacizu.txt (11274B)


      1 [1] Katz Got Your Tongue
      2 
      3   • [3]Home
      4   • [4]About
      5   • [5]Projects
      6   • [6]Talks
      7   • [7]Podcasts
      8   • [8]Schedule
      9 
     10 Login Subscribe
     11 Jan 9, 2012 6 min read
     12 
     13 JavaScript Needs Blocks
     14 
     15 While reading Hacker News posts about JavaScript, I often come across the
     16 misconception that Ruby's blocks are essentially equivalent to JavaScript's
     17 "first class functions". Because the ability to pass functions around,
     18 especially when you can create them anonymously, is extremely powerful, the
     19 fact that both JavaScript and Ruby have a mechanism to do so makes it natural
     20 to assume equivalence.
     21 
     22 In fact, when people talk about why Ruby's blocks are different from Python's
     23 functions, they usually talk about anonymity, something that Ruby and
     24 JavaScript share, but Python does not have. At first glance, a Ruby block is an
     25 "anonymous function" (or colloquially, a "closure") just as a JavaScript
     26 function is one.
     27 
     28 This impression, which I admittedly shared in my early days as a Ruby/
     29 JavaScript developer, misses an important subtlety that turns out to have large
     30 implications. This subtlety is often referred to as "Tennent's Correspondence
     31 Principle". In short, Tennent's Correspondence Principle says:
     32 
     33     "For a given expression expr, lambda expr should be equivalent."
     34 
     35 This is also known as the principle of abstraction, because it means that it is
     36 easy to refactor common code into methods that take a block. For instance,
     37 consider the common case of file resource management. Imagine that the block
     38 form of File.open didn't exist in Ruby, and you saw a lot of the following in
     39 your code:
     40 
     41 begin
     42   f = File.open(filename, "r")
     43   # do something with f
     44 ensure
     45   f.close
     46 end
     47 
     48 In general, when you see some code that has the same beginning and end, but a
     49 different middle, it is natural to refactor it into a method that takes a
     50 block. You would write a method like this:
     51 
     52 def read_file(filename)
     53   f = File.open(filename, "r")
     54   yield f
     55 ensure
     56   f.close
     57 end
     58 
     59 And you'd refactor instances of the pattern in your code with:
     60 
     61 read_file(filename) do |f|
     62   # do something with f
     63 end
     64 
     65 In order for this strategy to work, it's important that the code inside the
     66 block look the same after refactoring as before. We can restate the
     67 correspondence principle in this case as:
     68 
     69     ```ruby # do something with f ```
     70 
     71     should be equivalent to:
     72 
     73     do
     74       # do something with
     75     end
     76 
     77 At first glance, it looks like this is true in Ruby and JavaScript. For
     78 instance, let's say that what you're doing with the file is printing its mtime.
     79 You can easily refactor the equivalent in JavaScript:
     80 
     81 try {
     82   // imaginary JS file API
     83   var f = File.open(filename, "r");
     84   sys.print(f.mtime);
     85 } finally {
     86   f.close();
     87 }
     88 
     89 Into this:
     90 
     91 read_file(function(f) {
     92   sys.print(f.mtime);
     93 });
     94 
     95 In fact, cases like this, which are in fact quite elegant, give people the
     96 mistaken impression that Ruby and JavaScript have a roughly equivalent ability
     97 to refactor common functionality into anonymous functions.
     98 
     99 However, consider a slightly more complicated example, first in Ruby. We'll
    100 write a simple class that calculates a File's mtime and retrieves its body:
    101 
    102 class FileInfo
    103   def initialize(filename)
    104     @name = filename
    105   end
    106 
    107   # calculate the File's +mtime+
    108   def mtime
    109     f = File.open(@name, "r")
    110     mtime = mtime_for(f)
    111     return "too old" if mtime < (Time.now - 1000)
    112     puts "recent!"
    113     mtime
    114   ensure
    115     f.close
    116   end
    117 
    118   # retrieve that file's +body+
    119   def body
    120     f = File.open(@name, "r")
    121     f.read
    122   ensure
    123     f.close
    124   end
    125 
    126   # a helper method to retrieve the mtime of a file
    127   def mtime_for(f)
    128     File.mtime(f)
    129   end
    130 end
    131 
    132 We can easily refactor this code using blocks:
    133 
    134 class FileInfo
    135   def initialize(filename)
    136     @name = filename
    137   end
    138 
    139   # refactor the common file management code into a method
    140   # that takes a block
    141   def mtime
    142     with_file do |f|
    143       mtime = mtime_for(f)
    144       return "too old" if mtime < (Time.now - 1000)
    145       puts "recent!"
    146       mtime
    147     end
    148   end
    149 
    150   def body
    151     with_file { |f| f.read }
    152   end
    153 
    154   def mtime_for(f)
    155     File.mtime(f)
    156   end
    157 
    158 private
    159   # this method opens a file, calls a block with it, and
    160   # ensures that the file is closed once the block has
    161   # finished executing.
    162   def with_file
    163     f = File.open(@name, "r")
    164     yield f
    165   ensure
    166     f.close
    167   end
    168 end
    169 
    170 Again, the important thing to note here is that we could move the code into a
    171 block without changing it. Unfortunately, this same case does not work in
    172 JavaScript. Let's first write the equivalent FileInfo class in JavaScript.
    173 
    174 // constructor for the FileInfo class
    175 FileInfo = function(filename) {
    176   this.name = filename;
    177 };
    178 
    179 FileInfo.prototype = {
    180   // retrieve the file's mtime
    181   mtime: function() {
    182     try {
    183       var f = File.open(this.name, "r");
    184       var mtime = this.mtimeFor(f);
    185       if (mtime < new Date() - 1000) {
    186         return "too old";
    187       }
    188       sys.print(mtime);
    189     } finally {
    190       f.close();
    191     }
    192   },
    193 
    194   // retrieve the file's body
    195   body: function() {
    196     try {
    197       var f = File.open(this.name, "r");
    198       return f.read();
    199     } finally {
    200       f.close();
    201     }
    202   },
    203 
    204   // a helper method to retrieve the mtime of a file
    205   mtimeFor: function(f) {
    206     return File.mtime(f);
    207   }
    208 };
    209 
    210 If we try to convert the repeated code into a method that takes a function, the
    211 mtime method will look something like:
    212 
    213 function() {
    214   // refactor the common file management code into a method
    215   // that takes a block
    216   this.withFile(function(f) {
    217     var mtime = this.mtimeFor(f);
    218     if (mtime < new Date() - 1000) {
    219       return "too old";
    220     }
    221     sys.print(mtime);
    222   });
    223 }
    224 
    225 There are two very common problems here. First, this has changed contexts. We
    226 can fix this by allowing a binding as a second parameter, but it means that we
    227 need to make sure that every time we refactor to a lambda we make sure to
    228 accept a binding parameter and pass it in. The var self = this pattern emerged
    229 in JavaScript primarily because of the lack of correspondence.
    230 
    231 This is annoying, but not deadly. More problematic is the fact that return has
    232 changed meaning. Instead of returning from the outer function, it returns from
    233 the inner one.
    234 
    235 This is the right time for JavaScript lovers (and I write this as a sometimes
    236 JavaScript lover myself) to argue that return behaves exactly as intended, and
    237 this behavior is simpler and more elegant than the Ruby behavior. That may be
    238 true, but it doesn't alter the fact that this behavior breaks the
    239 correspondence principle, with very real consequences.
    240 
    241 Instead of effortlessly refactoring code with the same start and end into a
    242 function taking a function, JavaScript library authors need to consider the
    243 fact that consumers of their APIs will often need to perform some gymnastics
    244 when dealing with nested functions. In my experience as an author and consumer
    245 of JavaScript libraries, this leads to many cases where it's just too much
    246 bother to provide a nice block-based API.
    247 
    248 In order to have a language with return (and possibly super and other similar
    249 keywords) that satisfies the correspondence principle, the language must, like
    250 Ruby and Smalltalk before it, have a function lambda and a block lambda.
    251 Keywords like return always return from the function lambda, even inside of
    252 block lambdas nested inside. At first glance, this appears a bit inelegant, and
    253 language partisans often accuse Ruby of unnecessarily having two types of
    254 "callables", in my experience as an author of large libraries in both Ruby and
    255 JavaScript, it results in more elegant abstractions in the end.
    256 
    257 Iterators and Callbacks
    258 
    259 It's worth noting that block lambdas only make sense for functions that take
    260 functions and invoke them immediately. In this context, keywords like return,
    261 super and Ruby's yield make sense. These cases include iterators, mutex
    262 synchronization and resource management (like the block form of File.open).
    263 
    264 In contrast, when functions are used as callbacks, those keywords no longer
    265 make sense. What does it mean to return from a function that has already
    266 returned? In these cases, typically involving callbacks, function lambdas make
    267 a lot of sense. In my view, this explains why JavaScript feels so elegant for
    268 evented code that involves a lot of callbacks, but somewhat clunky for the
    269 iterator case, and Ruby feels so elegant for the iterator case and somewhat
    270 more clunky for the evented case. In Ruby's case, (again in my opinion), this
    271 clunkiness is more from the massively pervasive use of blocks for synchronous
    272 code than a real deficiency in its structures.
    273 
    274 Because of these concerns, the ECMA working group responsible for ECMAScript,
    275 TC39, [12]is considering adding block lambdas to the language. This would mean
    276 that the above example could be refactored to:
    277 
    278 FileInfo = function(name) {
    279   this.name = name;
    280 };
    281 
    282 FileInfo.prototype = {
    283   mtime: function() {
    284     // use the proposed block syntax, `{ |args| }`.
    285     this.withFile { |f|
    286       // in block lambdas, +this+ is unchanged
    287       var mtime = this.mtimeFor(f);
    288       if (mtime < new Date() - 1000) {
    289         // block lambdas return from their nearest function
    290         return "too old";
    291       }
    292       sys.print(mtime);
    293     }
    294   },
    295 
    296   body: function() {
    297     this.withFile { |f| f.read(); }
    298   },
    299 
    300   mtimeFor: function(f) {
    301     return File.mtime(f);
    302   },
    303 
    304   withFile: function(block) {
    305     try {
    306       var f = File.open(this.name, "r");
    307       block(f);
    308     } finally {
    309       f.close();
    310     }
    311   }
    312 };
    313 
    314 Note that a parallel proposal, which replaces function-scoped var with
    315 block-scoped let, will almost certainly be accepted by TC39, which would
    316 slightly, but not substantively, change this example. Also note block lambdas
    317 automatically return their last statement.
    318 
    319 Our experience with Smalltalk and Ruby show that people do not need to
    320 understand the SCARY correspondence principle for a language that satisfies it
    321 to yield the desired results. I love the fact that the concept of "iterator" is
    322 not built into the language, but is instead a consequence of natural block
    323 semantics. This gives Ruby a rich, broadly useful set of built-in iterators,
    324 and language users commonly build custom ones. As a JavaScript practitioner, I
    325 often run into situations where using a for loop is significantly more
    326 straight-forward than using forEach, always because of the lack of
    327 correspondence between the code inside a built-in for loop and the code inside
    328 the function passed to forEach.
    329 
    330 For the reasons described above, I strongly approve of [13]the block lambda
    331 proposal and hope it is adopted.
    332 
    333 [14]
    334 
    335 Published by:
    336 
    337 [15] Yehuda Katz
    338 [16]
    339 Katz Got Your Tongue © 2024
    340 [17]Powered by Ghost
    341 [pixel]
    342 
    343 References:
    344 
    345 [1] https://yehudakatz.com/
    346 [3] http://www.yehudakatz.com/
    347 [4] https://yehudakatz.com/about/
    348 [5] https://yehudakatz.com/projects/
    349 [6] https://yehudakatz.com/talks/
    350 [7] https://yehudakatz.com/podcasts/
    351 [8] https://yehudakatz.com/schedule/
    352 [12] http://wiki.ecmascript.org/doku.php?id=strawman%3Ablock_lambda_revival&ref=yehudakatz.com
    353 [13] http://wiki.ecmascript.org/doku.php?id=strawman%3Ablock_lambda_revival&ref=yehudakatz.com
    354 [14] https://yehudakatz.com/2011/12/12/amber-js-formerly-sproutcore-2-0-is-now-ember-js/
    355 [15] https://yehudakatz.com/author/wycats/
    356 [16] https://yehudakatz.com/2012/04/13/tokaido-my-hopes-and-dreams/
    357 [17] https://ghost.org/