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