Rather than using promises, FileIO starts with an API using only callback functions. This is an efficient approach, and a little lighter than a promise style. A promise API can be trivially implemented on top of an API based on callbacks. The callback API leads to a continuation passing style, where rather than calling functions and manipulating their return values, we call a function and provide it with a callback (or continuation) to handle the data.
var fs = require('fs-continuable')
, sys = require('sys')
var continuable = fs.readFile('/etc/passwd','ascii')
// readFile returns a continuable, which can be continued by passing a continuation to it
// no I/O happens until you provide a continuation, so a continuable *can be continued* but simply creating it does not perform any I/O.
// the continuation is a function that accepts whatever value the asynchronous action eventually produces
// in this case readFile produces the contents of the file, which we will just display
function display(x){sys.inspect(x)}
// now to do some actual I/O, we pass the continuation to the continuable, causing it to continue:
continuable(display)
// the same could have been done all on one line as:
fs.readFile('/etc/passwd','ascii')(function(x){sys.inspect(x)})
The fs-continuable module exposes all asynchronous operations as functions that return a continuable. A continuable is a function which takes a continuation as an argument, and then perhaps does some asynchronous I/O, and calls the continuation with the result. Here "continuation" can be read as a synonym for "callback function"; the only difference is one of emphasis.
The main difference between a promise style and continuable style is the sequencing of creating the asynchronous request and providing the handler function.
// promise style
var promise=readFile("11.txt")
promise.addCallback(function(data){print(data)})
// callback style
var continuable=readFile("11.txt")
continuable(function(data){print(data)})
In the promise style, the promise is first created, which, conceptually, immediately spawns some I/O process in the background. Then a handler is attached to the promise to deal with the event that will be emitted when the process completes. In the callback style, a continuable is returned, which can be passed around and manipulated, combined with other continuables, and so on, much like a promise, but unlike a promise, no I/O is performed until the continuation is provided. On the second line, the function that prints the data is provided, and the asynchronous I/O request is made, and eventually the continuation is called with the contents of the file...
...or, perhaps, an error.
In the example above we neglected to show how errors are dealt with. Any I/O action can potentially fail, for example readFile will fail if the file does not exist.
// handlers for success and failure
function successHandler(data){
print("success!\n")
print(data)}
function failureHandler(error){
print("failure:\n")
print(error)}
// promise style
var promise=readFile("11.txt")
promise.addCallback(successHandler)
promise.addErrback(failureHandler)
// callback style
var continuable=readFile("11.txt")
continuable(either(failureHandler,successHandler))
In the promise case, one of two events will be emitted, either a success event, or a failure event.
We have the option of adding event handlers for either, neither, or both.
In the callback style implemented here, there is only one callback function which receives either a success result or a failure result.
In this style it becomes easier to deal with errors than to ignore them, which encourages good habits.
To distinguish in the handler between success and failure we use the Either type and helper functions like either.
The Either type is an idiom borrowed directly from Haskell's beautiful type system, where it is commonly used to deal with operations that may either succeed or fail, which is exactly what we have here. (If you doubt that a type system can be beautiful, or if "static typing" makes you think of Java or C++, I highly recommend Haskell!)
In the example above, the continuation passed will be called with a value of type Either Error String.
This means either an Error or a String, and the ability to tell them apart at runtime unambiguously.
More generally, a value of type Either a b is either a value of type a, called a Left, or of type b, called a Right, where a and b can be any types at all.
At runtime, what you do with an Either value is test whether it is a Left or Right, and then extract the value of the corresponding type from it.
The Either type is a powerful and practical tool. In JavaScript an Either return value does away with messy and unpredictable type checking, and leads to simple and elegant APIs. JavaScript programmers expend considerable effort to determine at runtime what the type of some value is, and sometimes this is quite hard or even impossible. In my experience, JavaScript programming is greatly simplified by the simple discipline of doing away with runtime type checks altogether. (The exception is generally at API boundaries, where type checking is a convenience to the user, to catch errors in the use of the API and fail as early as possible with an appropriate error message.)
The either module is a simple Either implementation which exports Left and Right constructors, isLeft and isRight tests, and an either convenience function which takes a function to handle a left value of type a and a function to handle a right value of type b, and returns a function that handles an Either a b.
If we want to explicitly deal with the Either type in the example above, we could pass a callback which tests whether the argument is a Left or Right and then deals with the error or a success result. Instead here we used the either() helper function, which takes two functions, one for each of the Left and Right types, and returns a function that handles an Either value by testing it and then dispatching to the appropriate function.
The type of each function is given in a notation similar to that used in Haskell. Unlike in Haskell, the type annotations are not part of the program and are not verified by the compiler, but they provide a hint as to how an API method is intended to be used. For "::" read "of type" or "has the type", and for "a → b" read "function taking a and returning b". Where a value is ignored or will be undefined, an underscore is used.
There are wrappers for the methods of the node 'fs' module, all of these take the same arguments as the regular node functions, and passes the error or success result from node wrapped in the Either type:
rename :: (String, String) → Continuable Either Error _
truncate :: (Fd, length::Int) → Continuable Either Error _
chmod :: (path::String, mode::Int) → Continuable Either Error _
stat :: String → Continuable Either Error StatResult
unlink :: String → Continuable Either Error _
rmdir :: String → Continuable Either Error _
mkdir :: (String, Int) → Continuable Either Error _
readdir :: String → Continuable Either Error [String]
close :: Fd → Continuable Either Error _
open :: (String, Int, Int) → Continuable Either Error Fd
write :: (Fd, String, pos::Int, encoding::String) → Continuable Either Error Int
read :: (Fd, length::Int, position::Int, encoding::String) → Continuable Either Error String
On these low-level functions we build some higher-level abstractions:
touch :: (String, mode::Opt Int) → Continuable Either Error _
readFile :: (path::String, encoding::String) → Continuable Either Error String
writeFile :: (path::String, data::String, encoding::Opt String, mode::Opt Int) → Continuable Either Error _
appendFile :: (String, String, Opt String, Opt Int) → Continuable Either Error _
copyFile :: (src::String, dest::String) → Continuable Either Error _
Run node test.js to test these.
The continuable module contains two functions for dealing with lists of continuables:
parallel :: [Continuable x] → Continuable [x]
sequence :: [Continuable x] → Continuable [x]
Both of these take a list of Continuables and return a Continuable which runs all of them and returns the accumulated results in a list. The difference is that parallel runs all of them at once, while sequence runs each only after the previous has returned. Both are completely agnostic as to the type of the returned results; specifically they can be used with Continuable Eithers but do not require them or do any special handling of Left values.
The either module contains constructors, tests, and convenience functions for working with the Either type.
Left :: a → Either a b
Right :: b → Either a b
isLeft :: (Either a b) → Boolean
isRight :: (Either a b) → Boolean
either :: (a → c) → (b → c) → (Either a b → c)
applyLeft :: (Either a b → c) → (a → c) → (Either a b → c)
applyRight :: (Either a b → c) → (b → c) → (Either a b → c)
There are also some stream functions and a stream API exported by fs-continuable. See the source for details or look at doc/streams.
This version works requires the latest node v0.1.30.
First download the code, then copy the *.js files to your ~/.node_libraries or somewhere else in your require path.
Run node test.js and you should see all tests passed.
If you want to check out the development code instead of the stable release, you can use the update.sh script in the source directory:
$ curl http://boshi.inimino.org/3box/fileIO/update.sh | sh