Beautiful Off Main Thread File I/O
October 18, 2012 § 7 Comments
Now that the main work on Off Main Thread File I/O for Firefox is complete, I have finally found some time to test-drive the combination of Task.js and OS.File. Let me tell you one thing: it rocks!
Rather than a long discussion, let me show you a nice example Off Main Thread File I/O, written with the partial implementation of Task.js already present in the source code of the Mozilla Platform. The following snippet implements copying a file to another file by chunks:
let copy = function copy(sourcePath, destPath, chunkSize) { let sourceFile, destFile; return Task.spawn(function() { try { // Open input, output files sourceFile = yield OS.File.open(sourcePath); destFile = yield OS.File.open(destPath, {write: true, trunc: true}); // Start copying let buffer = new Uint8Array(chunkSize); while(true) { let bytes = yield sourceFile.readTo(buffer); if (bytes == 0) return; let view; if (bytes < buffer.byteLength) { view = buffer.subarray(0, bytes); } else { view = buffer; } yield destFile.write(view); } } catch(x) { console.log("Copy failed", x); } finally { // Don't forget to close the files (double-closing is fine) if (destFile) { destFile.close(); } if (sourceFile) { sourceFile.close(); } }); };
Each occurrence of yield accept as right-hand side an expression that returns a promise, and produces as left-hand side the result with which the promise is resolved. Nice and readable.
For comparison, the following implements the same function without Task.js:
let copy2 = function copy2(sourcePath, destPath, chunkSize) { // Open source file; let sourceFile; let promise = OS.File.open(sourcePath); promise = promise.then( function onSuccess(file) { sourceFile = file; } ); // Open destination file let destFile; promise = promise.then( function() { return OS.File.open(destPath, {write: true, trunc: true}); } ); promise = promise.then( function onSuccess(file) { destFile = file; } ); // Prepare loop let buffer = new Uint8Array(chunkSize); let loop = function loop() { // Read chunk let promise = sourceFile.readTo(buffer); promise = promise.then( function onSuccess(bytes) { if (bytes == 0) { // Copy is complete return Promise.resolve(true); } let view; // Write chunk if (bytes < buffer.byteLength) { view = buffer.subarray(0, bytes); } else { view = buffer; } return destFile.write(view); } ); // And loop promise = promise.then(loop); return promise; }; // Start loop promise = promise.then( function onSuccess() { return loop(0); } ); // Close files promise = promise.then( function onSuccess() { return sourceFile.close(); } ); promise = promise.then( function onSuccess() { return destFile.close(); } ); // Also close files in case of error promise = promise.then( null, function onFailure(reason) { console.error("Copy failed", reason); if (sourceFile) { sourceFile.close(); } if (destFile) { destFile.close(); } throw reason; } ); return promise; };
While the use of promises make this code much more readable than the spaghetti callbacks generally involved in asynchronous algorithms, in this case, using Task.js divides the line count by 3 and lets us use our beloved while loop.
Bottom line: I am quite excited about Task.js.
Research bottom line: Also, I have the feeling that the idea behind Task.js could be transposed to other forms of monads that do not necessarily involve asynchronicity or concurrency. I am sure that some ingenious mind will find a way to weaponize JavaScript generators into something even better than Task.js
Edit I had forgotten to close the files, this is now fixed. I took the opportunity to add a demonstration of error-handling.
Edit 2 After David Herman’s remark, I updated error handling to make it even nicer.
The above example definitely looks really, really nice, but it’s hard to evaluate without seeing what the error handling looks like.
I have just added some error-handling to both examples.
Error handling is one of the killer features of task.js — whereas with promises you have to register an error handler, with task.js you can once again use try-catch.
Wow, that’s just great!
I have just updated the example based on your remark.
JS asynchrony is monadic, and promises are at least similar in structure to a monadic type (I’m not quite sure whether they’re an exact fit). You can structure async I/O as a monad and the Roy programming language actually provides do-notation for this purpose: http://en.wikipedia.org/wiki/Monad_%28functional_programming%29#do-notation Brian McKenna also has experimented with providing do-notation for JS using sweet.js: https://github.com/mozilla/sweet.js/wiki/Example-macros
But in fact, you can pretty much look at task.js as providing the same thing as do-notation via generators.
Yes, this was my intuition.
I can imagine a definition of Task along the lines of:
let Task = new Monad();
Task.bind = function bind(x, k) {
return Promise.resolve(x).then(k);
}
Task.result = function result(x) {
return Promise.resolve(x);
}
Task.fail = function fail(reason) {
return Promise.reject(reason);
}
// ...
I have just opened a student project on the topic: https://github.com/Yoric/Mozilla-Student-Projects/issues/41