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/