Skip to content

Instantly share code, notes, and snippets.

@Raynos
Forked from wycats/promises.js
Last active August 18, 2022 22:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Raynos/5279565 to your computer and use it in GitHub Desktop.
Save Raynos/5279565 to your computer and use it in GitHub Desktop.
// The well-known callback pattern
fs.mkdir("some-path", function(err) {
fs.open("some-path/file", "w", function(err, fd) {
var buffer = new Buffer("hello world")
fs.write(fd, buffer, 0, buffer.length, null, function(err, bytes) {
console.log("Wrote " + bytes + " bytes")
})
})
})
// What if we changed all the APIS to return a function that takes the callback
// It's the exact same as node core except we have a return value of a callback
function mkdir(uri) {
return function (cb) {
fs.mkdir(uri, cb)
}
}
function open(uri, flags) {
return function (cb) {
fs.open(uri, flags, cb)
}
}
function write(fd, buffer, offset, length, position) {
return function (cb) {
fs.write(fd, buffer, offset, length, position, cb)
}
}
// Nothing is stopping you from doing the same thing with partial functions.
// Slightly different syntax, but the same pattern. Nothing major
// new to learn here.
mkdir("some-path")(function (err) {
open("some-path/file", "w")(function (err, fd) {
var buffer = new Buffer("hello world")
write(fd, buffer, 0, buffer.length, null)(function (err, bytes) {
console.log("Wrote " + bytes + " bytes")
})
})
})
// Want pipeable sugar. Sure that's easy.
// We introduce the notion of a duplex callback. It's a callback that takes
// a callback and returns a callback!
function pipeable(future) {
future.pipe = function (duplex) {
return pipeable(duplex(future))
}
return future
}
pipeable(mkdir("some-path"))
// Of course we don't have the file, that would be magic!
// Instead we have a callback that we can call. So here lets
// Return a callback invoke the file callback, then open the fd and
// pass it to the callback
.pipe(function (file) {
return function future(callback) {
file(function (err) {
open("some-path/file", "w")(callback)
})
}
})
.pipe(function (fd) {
return function future(callback) {
fd(function (err, fd) {
var buffer = new Buffer("hello world")
write(fd, buffer, 0, buffer.length, null)(callback)
})
}
})
(function (err, bytes) {
console.log("Wrote " + bytes + " bytes")
})
// Wow that's noisy! But it's re-occuring everywhere. Can we write a function
// for this?
function map(lambda) {
return function (source) {
return function future(callback) {
source(function (err, value) {
lambda(err, value)(callback)
})
}
}
}
pipeable(mkdir("some-path"))
.pipe(map(function (err) {
return open("some-path/file", "w")
}))
.pipe(map(function (err, fd) {
var buffer = new Buffer("hello world")
return write(fd, buffer, 0, buffer.length, null)
}))
// this is still a bit weird!
(function (err, bytes) {
console.log("Wrote " + bytes + " bytes")
})
// let's update pipeable to support half duplex callbacks. That is
// a callback that takes a callback and doesn't return one
function pipeable(future) {
if (!future) return
future.pipe = function (duplex) {
return pipeable(duplex(future))
}
return future
}
pipeable(mkdir("some-path"))
.pipe(map(function (err) {
return open("some-path/file", "w")
}))
.pipe(map(function (err, fd) {
var buffer = new Buffer("hello world")
return write(fd, buffer, 0, buffer.length, null)
}))
.pipe(function (req) {
// not quite optimum!!
req(function (err, bytes) {
console.log("Wrote " + bytes + " bytes")
})
})
// Let's create a little sink function for this
function sink(callback) {
return function future(source) {
source(callback)
}
}
pipeable(mkdir("some-path"))
.pipe(map(function (err) {
return open("some-path/file", "w")
}))
.pipe(map(function (err, fd) {
var buffer = new Buffer("hello world")
return write(fd, buffer, 0, buffer.length, null)
}))
.pipe(sink(function (err, bytes) {
console.log("Wrote " + bytes + " bytes")
})
// BUT WHAT ABOUT ERROR HANDLING >:(
// Actually that's not hard either. Just have map pass on the
// err to the callback instead of to the lambda.
function map(lambda) {
return function (source) {
return function future(callback) {
source(function (err, value) {
if (err) {
return callback(err)
}
lambda(value)(callback)
})
}
}
}
// Now just drop the errors everywhere. They will bubble up to
// the last item in the chain by default, which is the sink
pipeable(mkdir("some-path"))
.pipe(map(function () {
return open("some-path/file", "w")
}))
.pipe(map(function (fd) {
var buffer = new Buffer("hello world")
return write(fd, buffer, 0, buffer.length, null)
}))
.pipe(sink(function (err, bytes) {
console.log("Wrote " + bytes + " bytes")
})
// Now the most important part of this is that this is STILL callbacks
// Our fancy pants chaining sugar & higher order functions are 100%
// compatible with everything else in node core & npm.
function readAndWriteFile(folder, file, callback) {
pipeable(mkdir(folder))
.pipe(map(function () {
return open(path.join(folder, file), "w")
}))
.pipe(map(function (fd) {
var buffer = new Buffer("hello world")
return write(fd, buffer, 0, buffer.length, null)
}))
.pipe(sink(callback)
}
// Or maybe let's get rid of that pipeable chaining sugar.
function readAndWriteFile(folder, file, callback) {
var dir = mkdir(folder)
var fd = map(function (directory) {
return open(path.join(folder, file), "w")
})(dir)
map(function (fd) {
var buffer = new Buffer("hello world")
return write(fd, buffer, 0, buffer.length, null)
})(fd)(callback)
}
@dominictarr
Copy link

I looked into futures once, but I wanted an easy way to say, these(a, b).then(that) there was nothing infront of my face. just lots of text.

this gist has the right amount of code vs. text.

Hmm, Actually, I don't think that sequential is the painful case in async control flow, the painful case is to do x,y,z in parallel. Usually, you have a fixed number of things you want to do in series, like, stat a file, then create a directory, then create a file... but much more often you have a list of things you want to do in parallel (like, delete every file)

Also, I like how this is lazy by default.

@Raynos
Copy link
Author

Raynos commented Apr 3, 2013

parallel is trivial for the trivial case where you want to take a list of paaf's and return a single paaf containing a list of values.

The only problem with parallel is multiple error semantics :/

// for arguments sake let's call `( (Any -> void) -> void )`  `Future Any`
// list :: [Future Any] -> Future [Any]
function list(futures) {
    return function future(cb) {
        var result = [], finished, count = 0
        futures.forEach(function (source, index) {
            source(function (err, value) {
                if (err && !finished) {
                    finished = true
                    return cb(err)
                } else if (!err) {
                    result[index] = value
                    if (++count === futures.length) {
                        cb(null, result)
                    }
                }
            })
        })
    }
}

@Gozala
Copy link

Gozala commented Apr 4, 2013

@Raynos @dominictarr this is just a minimal promise implementation, that misses or omits lot's of things that proper promise implementations solve:

  • Runtime exceptions have to be handled differently and can't really be handled for async case.
  • Laziness, which is either good or bad depending on use case.
  • Each consumer restarts task (same as reducers & @Raynos was one to complain :)
  • Callback may be invoked more than ones and this pattern makes no guarantees about it.
  • No value propagation (with promises if you're handler returns promise it's resolved to that promise resolution value).
  • No way to distinguish result value from ordinary function.

@jkroso
Copy link

jkroso commented Apr 4, 2013

@creationix had a similar idea with a library called do

@creationix
Copy link

I'll actually be going into more details about my do/continuable idea that's a lot like this for my realtime.eu talk in couple weeks.

But related to the proposal, I've always wished node had gone this style instead of the inline callback-last style. Yes, this is a minimal promise with a lot less gurantees, but also a lot less overhead and complexity. It's a meet-in-the-middle API.

I experimented heavily with this in lua in combination with coroutines and am currently experimenting with node-fibers. This style of minimal promise is enough to implement something like task.js faux blocking as long as you have coroutines or powerful enough generators.

For example, piping a stream in my new API style is a simple do..while
loop because readable streams are just objects with a .read()(callback) method, and writable streams are just objects with a .write(chunk)(callback) signature.

var fd = await(fs.open("input.txt", "r"));
var readable = new fs.ReadStream(fd);
fd = await(fs.open("output.txt", "w"));
var writable = new fs.WriteStream(fd);

// Copy the file without needing a pipe helper because it's so trivial.
do {
  var chunk = await(readable.read());
  await(writable.write(chunk));
} while (chunk);

Look at my moonslice-* and continuable-* repos for more examples and actual implementations.

@Raynos
Copy link
Author

Raynos commented Apr 4, 2013

@Gozala

  • runtime exceptions are trolls. Simply do not throw exceptions ever.
  • Laziness. Promises are not lazy by default. A future / continuable / paaf does not do any kind of IO until it's given a callback.
  • Hubbing or caching by default is massively out of scope. Hubbing by default is in scope for stream APIs
  • promise then may be invoked more then once and there are no garauntees unless you wrap everything. The same applies to this
  • value propagation is massively out of scope. Just implement and use fmap
  • When would you ever want to distinguish a future value from a function. Give a use-case or it's out of scope.

@Gozala
Copy link

Gozala commented Apr 4, 2013

@Raynos I simply pointed out differences. I think this proposal has some ambiguity and I don't necessary like overloading functions too much. That being said I very much prefer this over current node callback style, either way we both know this not gonna happen ;)

@Gozala
Copy link

Gozala commented Apr 4, 2013

To be clear I'm also not big fan of Promise/A as it makes far too many decisions that's why I made https://github.com/Gozala/eventual

@dominictarr
Copy link

@Gozala, this is about experimentation, not about getting something better (aka "different") into node.

callbacks are a lowest common denominator, and no one can disagree what they are, or what they do.
Just use var fs = continuify(require('fs')).

@dominictarr
Copy link

All of these approaches are winsome/loosesome, the true test isn't whether it's simple or complex or how many lines is it to construct simple examples, but how does it allow you to implement complex real world use-cases...

So, to prove an idea like this, build something with it.

@Gozala
Copy link

Gozala commented Apr 4, 2013

Just use var fs = continuify(require('fs')).

You could it's just @mikeal will tell everyone to not use it because it's not idiomatic node :)

@dominictarr
Copy link

I think if you expose classic callbacks in your module, it doesn't really matter if you use something else internally,
because your user still gets to decide wether they want to promisify(module) or continuify(module)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment