Created
July 12, 2013 14:26
-
-
Save clausreinke/5984869 to your computer and use it in GitHub Desktop.
monadic javascript/typescript: promises and generators
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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