Skip to content

Instantly share code, notes, and snippets.

@clausreinke
Created July 12, 2013 14:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save clausreinke/5984869 to your computer and use it in GitHub Desktop.
Save clausreinke/5984869 to your computer and use it in GitHub Desktop.
monadic javascript/typescript: promises and generators
declare function setImmediate(cb);
declare function setTimeout(cb,n);
declare var process;
function nextTick(cb) {
if (typeof setImmediate == "function") {
setImmediate(cb);
} else if (typeof process == "object" && typeof process.nextTick == "function") {
process.nextTick(cb);
} else if (typeof setTimeout == "function") {
setTimeout(cb,0);
} else {
throw "no nextTick";
}
}
// monadic generators and promises, the latter with
// with synchronous and asynchronous resolution, rejection and mock operations;
// works with tsc (TS v0.9, playground or npm)
// claus reinke, 2013
/* abstract */
class Monad {
then(cb:(value)=>Monad):Monad {
throw "abstract then";
}
then_(cb:(value)=>any):Monad { // result-wrapping helper
return this.then( value => this["constructor"].of( cb(value) ) );
}
static of(value): Monad {
throw "abstract of";
}
static forIn(vs:any[],body:(value)=>Monad) {
// console.log("forIn ",vs);
return (vs.length===0)
? this.of([])
: body(vs[0]).then( r=>
this.forIn(vs.slice(1),body).then( rs=>
this.of( [r].concat(rs) ) ) )
} // TODO: lose the map aspect
static forOf(gen /*:{next:()=>ItrResult}*/,body /*:(value)=>Monad*/, result = false) {
var res = gen.next();
return (res.done)
? (result ? body(res.value).then( _=>this.of() ) : this.of())
// usually, we only want yield results, not end-of-run return
// TODO: do we ever want the return value here?
: body(res.value).then( _=>
this.forOf(res,body).then( _=>
this.of() ) )
}
}
class MonadId extends Monad {
constructor(private value) {
super();
}
then(cb:(value)=>Monad):Monad {
return cb(this.value)
}
static of(value) {
return new MonadId(value)
}
}
class MonadCont extends Monad {
constructor(public cont) {
super();
}
then(cb) {
return new MonadCont( c=>this.cont( v=>cb(v).cont(c) ) )
}
static of(value) {
return new MonadCont( c=>c(value) )
}
}
// slight difference here: functional API (ok)
class Generator extends Monad {
constructor(public steps) {
super();
}
then(cb:(value)=>Monad):Monad {
return new Generator(this.steps.concat([{then:cb}]))
}
next() {
var step, result, steps = this.steps.slice();
while (step = steps.shift()) {
if (step.hasOwnProperty("of")) {
result = step.of;
} else if (step.hasOwnProperty("then")) {
steps.unshift.apply(steps,step.then(result).steps);
} else { // yield
return {done:false,value:step.yield
,next:function(value) {return new Generator([{of:value}].concat(steps)).next()}}
}
}
return {done:true,value:result}
}
static yield(value) {
return new Generator([{yield:value}])
}
static of(value) {
return new Generator([{of:value}])
}
}
/* abstract */
class MonadError extends Monad {
then(cb:(value)=>MonadError):MonadError {
throw "abstract then";
}
then_(cb:(value)=>any):MonadError { // result-wrapping helper
return this.then( value => this["constructor"].of( cb(value) ) );
}
then2(cb:(value)=>MonadError = this["constructor"].of
,err:(error)=>MonadError = this["constructor"].of
):MonadError { // double callback helper
// NOTE: undefined for missing parameter (not null)!
return this.then_(this["constructor"].wrap( cb ))
.handle_(this["constructor"].wrap( err ))
.then(x=>x);
}
thenP(cb:(error,value)=>MonadError):MonadError { // paired outcome callback helper
return this.then_(this["constructor"].wrap( value => cb(null,value) ))
.handle_(this["constructor"].wrap( error => cb(error,null) ))
.then(x=>x);
}
static of(value): MonadError {
throw "abstract of";
}
static raise(error): MonadError {
throw "abstract throw";
}
handle(cb:(error)=>MonadError):MonadError {
throw "abstract handle";
}
handle_(cb:(error)=>any):MonadError { // result-wrapping helper
return this.handle( error => this["constructor"].of( cb(error) ) );
}
static wrap(cb) { // redirect exceptions to rejections
return (value) => {
try {
return cb(value);
} catch (e) {
return this.raise(e);
}
}
}
}
class Promise extends MonadError {
static of(value):Promise {
return <Promise>new ResolvedPromise(value);
}
static raise(error): Promise {
return <Promise>new RejectedPromise(error);
}
}
class ResolvedPromise extends Promise {
constructor(private value) {
super();
}
then(cb:(value)=>Promise):Promise {
return Promise.wrap(cb)(this.value)
}
handle(cb:(error)=>Promise):Promise {
return this;
}
}
class RejectedPromise extends Promise {
constructor(private error) {
super();
}
then(cb:(value)=>Promise):Promise {
return this;
}
handle(cb:(error)=>Promise):Promise {
return Promise.wrap(cb)(this.error);
}
}
class PendingPromise extends Promise {
constructor( private resolver:Resolver ) {
super();
}
then(cb:(value)=>Promise):Promise {
return this.resolver.handle(cb,null);
}
handle(cb:(error)=>Promise):Promise {
return this.resolver.handle(null,cb);
}
}
class Resolver {
private listeners = [];
public promise;
private resolved = false;
private value = undefined;
private error = undefined;
constructor() {
this.promise = new PendingPromise(this);
}
handle(cb:(value)=>Promise, err:(error)=>Promise):Promise {
if (typeof this.value != "undefined") {
return cb ? Promise.wrap(cb)(this.value) : this.promise;
} else if (typeof this.error != "undefined") {
return err ? Promise.wrap(err)(this.error) : this.promise;
} else {
var r = new Resolver();
var resolve = vn => ResolvedPromise.of( r.resolve( vn, true ) );
var reject = e => RejectedPromise.raise( r.reject( e, true ) );
if (cb) {
this.listeners.push( p =>
p.then(Promise.wrap(cb)).then( resolve ).handle( reject )
);
} else if (err) {
this.listeners.push( p =>
p.handle(Promise.wrap(err)).then( resolve ).handle( reject )
);
} else {
throw "Resolver.handle called without success or error callback";
}
return r.promise;
}
}
resolve(value,sync=false) {
if (this.resolved) throw "already resolved!";
var p = ResolvedPromise.of(value);
if (sync) {
this.resolved = true;
this.value = value;
this.listeners.forEach( l => l(p) );
this.listeners = null;
} else {
this.resolved = true; // avoid resolve conflicts ..
nextTick(()=>{
this.value = value; // .. but don't make value available early
this.listeners.forEach( l => l(p) );
this.listeners = null;
});
}
}
reject(error,sync=false) {
if (this.resolved) throw "already resolved!";
var p = RejectedPromise.raise(error);
if (sync) {
this.resolved = true;
this.error = error;
this.listeners.forEach( l => l(p) );
this.listeners = null;
} else {
this.resolved = true; // avoid resolve conflicts ..
nextTick(()=>{
this.error = error; // .. but don't make error available early
this.listeners.forEach( l => l(p) );
this.listeners = null;
});
}
}
}
/*
// synchronous and asynchronous dummy operations,
// with labeled trace output
var syncOp = prefix => value => (
console.log(prefix+"(sync)",value),
ResolvedPromise.of(prefix.split(" ")[1].toLowerCase())
);
var asyncOp = prefix => value => {
var r = new Resolver();
nextTick(()=>( console.log(prefix+"(async)",value),
r.resolve(prefix.split(" ")[1].toLowerCase())
));
return r.promise;
};
function test(label,prefix,sync,op) {
console.log("// "+label +" (prefix "+prefix+")");
var rA = new Resolver();
var pA = rA.promise;
var pB = pA.then( op(prefix+" B") );
var pC = pA.then( syncOp(prefix+" C") );
var pD = pB.then( op(prefix+" D") );
var pE = pB.then( syncOp(prefix+" E") );
var pF = pC.then( op(prefix+" F") );
var pG = pC.then( syncOp(prefix+" G") );
var pH = pA.then( op(prefix+" H") );
var pI = pA.then( syncOp(prefix+" I") );
pH.then_( x => x ).then_( x => console.log(prefix,x) );
console.log(prefix,1);
rA.resolve("a",sync);
console.log(prefix,2);
}
test("sync resolve, syncOp", ".", true, syncOp);
test("async resolve, asyncOp", "..", false, asyncOp);
console.log("// resolved pipeline (prefix |)");
ResolvedPromise.of(1).then_( x => { throw "oops" } )
.then_( x => console.log("|result:",x) )
.handle_( e => console.log("|error:",e) )
.then_( x => console.log("|result:",x) )
.handle_( e => console.log("|error:",e) );
console.log("// nested resolved pipeline (prefix ||)");
ResolvedPromise.of(ResolvedPromise.of("scary nested thing"))
.then_( p => ( console.log("||",p) , p ) )
.then( p => p )
.then_( v => console.log("||","no "+v) );
var nested = x => {
var r1 = new Resolver();
nextTick( () => { var r2 = new Resolver();
r1.resolve( r2.promise );
nextTick( () => r2.resolve( x ) );
} );
return r1.promise;
};
console.log("// nested async pipeline (prefix >)");
nested(42).then_( p => { console.log(">","react to outer promise level");
p.then_( x => console.log(">","react to inner level",x) );
console.log(">","now waiting for nested promise")
});
console.log("// paired outcome callback then (prefix ,)");
ResolvedPromise.of("better not raise 0...")
.thenP( (error,value) => {
if (error)
return ResolvedPromise.of(console.log(",caught:",error))
else
throw 13
})
.then_( v => console.log(",value:",v) )
.handle_( e => console.log(",error:",e) );
ResolvedPromise.of("better not raise 0...")
.thenP( (error,value) =>
error ? ResolvedPromise.of(console.log(",caught:",error))
: ResolvedPromise.of(value+10) )
.then_( v => console.log(",value:",v) )
.handle_( e => console.log(",error:",e) );
RejectedPromise.raise("better not raise 0...")
.thenP( (error,value) =>
error ? ResolvedPromise.of(console.log(",caught:",error))
: ResolvedPromise.of(value+10) )
.then_( v => console.log(",value:",v) )
.handle_( e => console.log(",error:",e) );
RejectedPromise.raise("better not raise 0...")
.thenP( (error,value) => {
if (error)
throw 7
else
return ResolvedPromise.of(value+10)
})
.then_( v => console.log(",value:",v) )
.handle_( e => console.log(",error:",e) );
console.log("// double callback then (prefix *)");
ResolvedPromise.of(0)
.then2( value => { throw 13; return <Promise>undefined }
, error => ResolvedPromise.of(console.log("*caught:",error)))
.then_( v => console.log("*value:",v) )
.handle_( e => console.log("*error:",e) );
ResolvedPromise.of(0)
.then2( value => ResolvedPromise.of(value+10)
, error => ResolvedPromise.of(console.log("*caught:",error)))
.then_( v => console.log("*value:",v) )
.handle_( e => console.log("*error:",e) );
RejectedPromise.raise(0)
.then2( value => ResolvedPromise.of(value+10)
, error => ResolvedPromise.of(console.log("*caught:",error)))
.then_( v => console.log("*value:",v) )
.handle_( e => console.log("*error:",e) );
RejectedPromise.raise(0)
.then2( value => ResolvedPromise.of(value+10)
, error => { throw 7; return <Promise>undefined })
.then_( v => console.log("*value:",v) )
.handle_( e => console.log("*error:",e) );
console.log("// double callback then, missing callbacks (prefix ?)");
ResolvedPromise.of(-272)
.then2( undefined
, error => ResolvedPromise.of(console.log("?error:",error)) )
.handle_( e => console.log("?caught:",e) )
.then_( v => console.log("?passed:",v) );
RejectedPromise.raise(-272)
.then2( value => ResolvedPromise.of(console.log("?value:",value))
, undefined )
.handle_( e => console.log("?caught:",e) )
.then_( v => console.log("?passed:",v) );
ResolvedPromise.of(-272)
.then2( value => ResolvedPromise.of(console.log("?value:",value))
, undefined )
.handle_( e => console.log("?caught:",e) )
.then_( v => console.log("?passed:",v) );
RejectedPromise.raise(-272)
.then2( undefined
, error => ResolvedPromise.of(console.log("?error:",error)) )
.handle_( e => console.log("?caught:",e) )
.then_( v => console.log("?passed:",v) );
console.log("// Promise.forIn (prefix :)");
Promise.forIn([1,2,3], i=> syncOp(":a "+i)(i)
.then(asyncOp(":b "+i))
.then(syncOp(":c "+i)) )
.then_( rs=>console.log(": "+rs) );
*/
console.log("\n// Generator.forIn, with yield, plain iteration (prefix #)");
var G = Generator;
var generator = ()=>G.forIn([1,2,3], i=> G.yield("yield1 "+i)
.then_( v=> console.log("(yield1 returns "+v+")" ) )
.then( _=> G.yield("yield2 "+i*i) )
.then_( v=> console.log("(yield2 returns "+v+")" ) )
.then_( _=>i )
);
var r = generator().next(), j = 0;
while (!r.done) {
console.log("# "+r.value);
r = r.next(j++);
}
console.log("# "+r.value);
// NOTE: forof doesn't feed values to its generator
console.log("\n// MonadId.forOf, same generator() (prefix ##)");
MonadId.forOf( generator(), y=> (console.log("## ",y), MonadId.of(y)) );
// TODO: add filter/map to interface
console.log("\n// MonadId.forOf, G.forOf, mapped&filtered generator() (prefix --)");
var generator2 = G.forOf( generator()
, y=> ( typeof y=="string" && y.match(/yield2/)
? G.yield(y.replace(/yield2 /,''))
: G.of(y) )
);
MonadId.forOf( generator2, y=> (console.log("-- "+y), MonadId.of(y)) );
console.log("\n// MonadId.forOf, iterTree recursive generator() (prefix *)");
function iterTree(tree) {
return Array.isArray(tree)
? tree.map( iterTree ).reduce( (x,y)=> x.then( _=> y ), G.of(undefined) )
: G.yield(tree);
}
var generator3 = iterTree([1,[],[[2,3],4],5]);
MonadId.forOf( generator3, y=> (console.log("* "+y), MonadId.of(y)) );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment