Skip to content

Instantly share code, notes, and snippets.

@hisui
Last active December 12, 2015 04:08
Show Gist options
  • Save hisui/4712504 to your computer and use it in GitHub Desktop.
Save hisui/4712504 to your computer and use it in GitHub Desktop.
Promise+Future for JavaScript
declare module sf {
export interface Future<T> extends AsyncEither<Error,T> {
lhs():FutureProjection_L<T>;
rhs():FutureProjection_R<T>;
flatMap<B>(f: (e:Either<Error,T>) => AsyncEither<Error,B>):Future<B>;
map<B>(f: (e:Either<Error,T>) => Either<Error,B>):Future<B>;
each(f: (e:Either<Error,T>) => void):Future<T>;
}
export interface Subject<T> extends Future<T> {
new ():Subject<T>;
notifyFail(e:Error):void;
notifyDone(o:T) :void;
}
export interface FutureProjection_L<A> {
flatMap<B>(f: (e:Error) => Future<B>):Future<B>;
map(f: (e:Error) => Error):Future<A>;
each(f: (e:Error) => void):Future<A>;
}
export interface FutureProjection_R<A> {
flatMap<B>(f: (e:A) => Future<B>):Future<B>;
map<B>(f: (e:A) => B):Future<B>;
each(f: (o:A) => void):Future<A>;
}
export interface EitherProjection_L<L,R> {
flatMap<B>(f: (e:L) => Either<B,R>):Either<B,R>;
map<B>(f: (e:L) => B):Either<B,R>;
each(f: (e:L) => void):Either<L,R>;
}
export interface EitherProjection_R<L,R> {
flatMap<B>(f: (e:R) => Either<L,B>):Either<L,B>;
map<B>(f: (e:R) => B):Either<L,B>;
each(f: (o:R) => void):Either<L,R>;
}
export interface AsyncEither<L,R> {
isCompleted():boolean;
cancel():void;
}
export interface Either<L,R> extends AsyncEither<L,R> {
lhs():EitherProjection_L<L,R>;
rhs():EitherProjection_R<L,R>;
}
}
// This is conceptual implementation of monadic promise/future system for JavaScript
// https://gist.github.com/hisui/4712504
(function ()
{
"use strict";
/**
* mix-in: モナドの基本的なオペレーションを提供します。
* @param M<A>
* @param A
*/
this["MonadOps"] || (
this["MonadOps"] = (function () {
// console.log("add monad ops => "+ this);
/**
* @param X
* @param e: X
* @return M<X>
*/
this.pure = function (e) { throw new Error("Unimplemented!") };
/**
* @param B
* @param f: A => M<B>
* @return M<B>
*/
this.flatMap = function (f) { throw new Error("Unimplemented!") };
/**
* @param B
* @param f: A => B
* @return M<B>
*/
this.map = function (f)
{
return this.flatMap(
(function (e) { return this.pure(f(e)) }).bind(this));
};
/**
* @param f: A => any
* @return M<A>
*/
this.each = function (f)
{
return this.map(function (e) { return (f(e), e) });
};
/**
* @param B
* @param f: M<A> => M<B>
* @return M<B>
*/
this.$ || (this.$ = function (f) { return f(this) });
/**
* @param (T..) = A
* @param f: (T..) => M<B>
* @return M<B>
*/
this.flatMapN = function (f)
{
return this.flatMap(function (args) { return f.apply(null, args) });
};
/**
* @param (T..) = A
* @param f: (T..) => B
* @return M<B>
*/
this.mapN = function (f)
{
return this.map(function (args) { return f.apply(null, args) });
};
/**
* @param (T..) = A
* @param f: (T..) => any
*/
this.eachN = function (f)
{
return this.each(function (args) { f.apply(null, args) });
};
/**
* @param B
* @param M<B> = A
* @return M<B>
*/
this.flatten = function () { return this.flatMap(function (e) { return e }) };
/**
* @return M<(A)>
*/
this.pack = function () { return this.map(function (e) { return [e] }) };
/**
* @param (T..) = A
* @param e: M<B>
* @return M<(T..,B)>
*/
this.push = function (that)
{
this.flatMap(function (a) { return that.map(function (b) { return [a, b] }) });
};
/**
* @param name: String
* @param args: Array<any>
* @return M<any>
*/
this.send = function (name, args)
{
return this.map(function (e) { return e[name].apply(e, args) });
};
}));
/**
* monkey-patching: MonadOps to Function
*/
(function (proto) {
MonadOps.call(proto);
/**
* @override MonadOps
*/
proto.flatMap = function (g)
{
var f = this;
return function ()
{
return g(f.apply(null, Array.prototype.slice.apply(arguments)))()
};
};
/**
* @override MonadOps
*/
proto.pure = function (e) { return function () { return e } };
/**
* 定義されてなかったらそれっぽいのを突っ込みます。
*/
proto.bind || (proto.bind = function (context)
{
var func = this;
var args = Array.prototype.slice.call(arguments, 1);
return (function ()
{
return func.apply(context, args.concat
(Array.prototype.slice.call(arguments)))
});
});
/**
*
*/
proto.extends = function (base, traits)
{
traits || (traits = {});
var ctor = this;
function stub() {
this.constructor = ctor;
}
for (var key in ctor.prototype) {
traits[key] || (traits[key] = ctor.prototype[key]);
}
stub.prototype = base.prototype;
ctor.prototype = new stub();
for (var key in traits) {
ctor.prototype[key] = traits[key];
}
return ctor;
};
/**
* @param T
* @return T => T
*/
Function.identity || (
Function.identity = function (e) { return e } );
})(Function.prototype);
/**
* 非同期の処理を表現します。
* @param V
*/
this["FutureLike"] || (
this["FutureLike"] = (function (base) {
function FutureLike()
{
base.call(this);
}
return FutureLike.extends(base,
{
/**
* この処理が終了しているかどうかです。
* @return Boolean
*/
isCompleted: function () { throw new Error("Not implemented!") },
/**
* この処理が終了した時に呼び出されるコールバックを設定します。
* @param f: V => any
*/
wait: function (f) { throw new Error("Not implemented!") },
/**
* 結果値を返します。この処理が終了していない場合、例外をスローします。
* @return V
*/
sync: function () { throw new Error("Not implemented!") },
/**
* この処理をキャンセルします。
*/
cancel: function () {}
});
})(Object));
/**
* mix-in: {@link FutureLike#sync()}が自分自身を返すような{@link FutureLike}です。
* @super FutureLike<T>
* @param T
*/
this["FutureThis"] || (
this["FutureThis"] = (function () {
/**
* override FutureLike<T>
*/
this.sync = function ()
{
if (!this.isCompleted()) {
throw new Error("This task is running!");
}
return this;
};
}));
/**
* mix-in: 常に終了した状態(isCompleted())の{@link FutureThis}です。
* @super FutureThis<T>
* @param T
*/
this["FutureEnds"] || (
this["FutureEnds"] = (function () {
/**
* @override FutureLike<T>
*/
this.isCompleted = function () { return true };
/**
* @override FutureLike<T>
*/
this.wait = function (f) { f(this) };
}));
/**
* 非同期の{@link Either}です。
* @super FutureLike<Either<L,R>>
* @param L
* @param R
*/
this["AsyncEither"] || (
this["AsyncEither"] = (function (base) {
function AsyncEither()
{
base.call(this);
}
return AsyncEither.extends(base,
{
/**
* @return AsyncEitherProjectionL<L,R>
*/
lhs: function () { throw new Error("Not implemented!") },
/**
* @return AsyncEitherProjectionR<L,R>
*/
rhs: function () { throw new Error("Not implemented!") },
/**
* "lhs" or "rhs"
* @return String
*/
dir: function () { throw new Error("Not implemented!") }
});
})(FutureLike));
/**
* 非同期の{@link Maybe}です。
* @super MonadOps<AsyncMaybe,T>, FutureLike<Maybe<T>>
* @param T
*/
this["AsyncMaybe"] || (
this["AsyncMaybe"] = (function (base) {
MonadOps.call(AsyncMaybe.prototype);
function AsyncMaybe()
{
base.call(this);
}
return AsyncMaybe.extends(base,
{
/**
* @return Boolean
*/
isDefined: function () { throw new Error("Not implemented!") },
/**
* @param B <: T
* @param f: => AsyncMaybe<B>
* @return AsyncMaybe<T>
*/
orElse: function (f)
{
return this.isDefined() ? this: f();
},
/**
* このオブジェクトが保持する値を返します。
* @return T
*/
get: function () { throw new Error("Not implemented!") },
/**
* @param e: T
* @return T
*/
getOr: function (e) { return this.isDefined() ? this.get(): e }
});
})(FutureLike));
/**
* オプショナルな値を格納します。
* @super AsyncMaybe<T>, FutureEnds<Maybe<T>>
* @param T
*/
this["Maybe"] || (
this["Maybe"] = (function (base) {
FutureThis.call(Maybe.prototype);
FutureEnds.call(Maybe.prototype);
function Some(e)
{
Maybe.call(this), this._raw = e;
}
Some.extends(Maybe,
{
/**
* @override MonadOps<AsyncMaybe,T>
*/
flatMap: function (f) { return f(this._raw) },
/**
* @override AsyncMaybe<T>
*/
get: function () { return this._raw },
/**
* @return String
*/
toString: function () { return "Some("+ this._raw +")" }
});
function None() { Maybe.call(this) }
None.extends(Maybe,
{
/**
* @override MonadOps<AsyncMaybe,T>
*/
flatMap: function (f) { return this },
/**
* @override Maybe<T>
*/
get: function () { throw new Error("No such element!") },
/**
* @return String
*/
toString: function () { return "None" }
});
FutureEnds.call(Maybe.prototype);
function Maybe(e)
{
if (!(this instanceof arguments.callee)) {
return arguments.length > 0 ? new Some(e): new None();
}
base.call(this);
}
return Maybe.extends(base,
{
/**
* @override MonadOps<AsyncMaybe,T>
*/
pure: function (e) { return new Some(e) },
/**
* @override AsyncMaybe<T>
*/
isDefined: function () { return this instanceof Some }
});
})(AsyncMaybe));
/**
* @super MonadOps<AsyncEither<L|R>,L|R>
* @param L
* @param R
*/
this["AsyncEitherProjection"] || (
this["AsyncEitherProjection"] = (function (base) {
function AsyncEitherProjection(src, dir)
{
if (!(dir === "lhs" || dir === "rhs")) {
throw new Error("bug? (*_*)");
}
base.call(this);
this._src = src;
this._dir = dir;
}
return AsyncEitherProjection.extends(base,
{
/**
* @override AsyncMaybe
*/
isDefined: function () { return this._src.dir() === this._dir },
/**
* @override AsyncMaybe
*/
get: function () { return this.sync().get() },
/**
* @override FutureLike<EitherProjection<L,R>>
*/
isCompleted: function () { return this._src.isCompleted() },
/**
* @override FutureLike<EitherProjection<L,R>>
*/
wait: function (f)
{
this._src.wait((function (e) { f(e[this._dir]) }).bind(this));
},
/**
* @override FutureLike<EitherProjection<L,R>>
*/
sync: function () { return this._src.sync()[this._dir]() },
/**
* @override FutureLike<EitherProjection<L,R>>
*/
cancel: function () { this._src.cancel() },
/**
* @return AsyncEither<L,R>
*/
e: function () { return this._src },
/**
* @override MonadOps<AsyncEither<L,R>,AsyncEither<L,R>>
*/
pure: function (e)
{
return Future[{rhs: "done", lhs: "fail"}[this._dir]](e)
},
/**
* @override MonadOps<AsyncEither<L,R>,AsyncEither<L,R>>
*/
flatMap: function (f)
{
var subj = new Subject();
this._src.wait((function (src)
{
src[this._dir]().flatMap(f).wait(subj.emit.bind(subj))
}).bind(this));
return subj;
},
/**
* @override MonadOps<AsyncEither<L,R>,AsyncEither<L,R>>
*/
push: function (that)
{
return Future.tuple(this.e(), that.e())
[this._dir]().mapN(function (a, b) { return a.concat(b) });
}
});
})(AsyncMaybe));
/**
* @super AsyncEitherProjection<L|R>, FutureEnds<EitherProjection>
* @param L
* @param R
*/
this["EitherProjection"] || (
this["EitherProjection"] = (function (base) {
FutureEnds.call(EitherProjection.prototype);
function EitherProjection(src, dir)
{
base.call(this, src, dir);
}
return EitherProjection.extends(base,
{
/**
* @override AsyncMaybe
*/
get: function () { return this.isDefined() ? this._src.raw(): Maybe().get() },
/**
* @override MonadOps<AsyncEither<L,R>,Either<L,R>>
*/
pure: function (e) { return Either[this._dir](e) },
/**
* @override MonadOps<AsyncEither<L,R>,Either<L,R>>
*/
flatMap: function (f) { return this.isDefined() ? f(this._src.raw()): this._src }
});
})(AsyncEitherProjection));
/**
* 左か右のどちらかの値を保持します。
* @super AsyncEither<L,R>, FutureEnds<Either>
* @param L
* @param R
*/
this["Either"] || (
this["Either"] = (function (base) {
FutureThis.call(Either.prototype);
FutureEnds.call(Either.prototype);
function Either(raw, dir)
{
if (!(dir === "lhs" || dir === "rhs")) {
throw new Error("bug? (*_*)");
}
base.call(this);
this._raw = raw;
this._dir = dir;
}
Either.lhs = function (e) { return new Either(e, "lhs") };
Either.rhs = function (e) { return new Either(e, "rhs") };
return Either.extends(base,
{
/**
* このオブジェクトが保持する値を返します。
* @internal
*/
raw: function () { return this._raw },
/**
* @override AsyncEither<L,R>
*/
lhs: function () { return new EitherProjection(this, "lhs") },
/**
* @override AsyncEither<L,R>
*/
rhs: function () { return new EitherProjection(this, "rhs") },
/**
* @override AsyncEither<L,R>
*/
dir: function () { return this._dir },
/**
* @return String
*/
toString: function () { return "Either("+ this._dir +":"+ this._raw +")" }
});
})(AsyncEither));
/**
* 完了後にエラーまたは値を生成する非同期処理を表現します。
* @super MonadOps<AsyncEither<Error,?>,Either<Error,T>>, AsyncEither<Error,T>
* @param T
*/
this["Future"] || (
this["Future"] = (function (base) {
function Future(e) {
if (e && !(e instanceof Either)) {
throw new Error("bug? (*_*) :: "+ e);
}
base.call(this);
this.done = e || null;
this.cons = function (e) {};
this.disp = 0;
}
Future.done = function (e) { return new Future(Either.rhs(e)) };
Future.fail = function (e) { return new Future(Either.lhs(e)) };
function cancelAll(l) {
for (var j = 0; j < l.length; ++j) {
if (l[j] !== null) l[j].cancel();
}
}
/**
* {@link Future#cancel()}した場合、エラーとして通知される値です。
*/
Future.canceled = new Error("cancel");
/**
* 終了しない{@link Future}を作成します。
* @return Future<Null>
*/
Future.never = function () { return new Subject() };
/**
* @param T..
* @param a..: Future<T>..
* @return Future<(T..)>
*/
Future.tuple = function () {
var subj = new Subject();
var done = 0;
var args = Array.prototype.slice.call(arguments);
var dest = [];
for (var i = 0; i < args.length; ++i) {
(function (i) {
args[i].get(function (e)
{
args[i] = null;
if (subj !== null) {
if (e.dir() === "lhs") {
subj.emit(e);
subj = null;
cancelAll(args);
return;
}
dest[i] = e.sync().raw();
if (++done == args.length) {
subj.notifyDone(dest);
subj = null;
}
}
})
})(i);
}
return subj;
};
/**
* @param T
* @param a..: Future<T>..
* @return Future<T>
*/
Future.first = function () {
var subj = new Subject();
var args = Array.prototype.slice.call(arguments);
for (var i = 0; i < args.length; ++i) {
(function (i) {
args[i].get(function (e)
{
args[i] = null;
if (subj !== null) {
subj.emit(e);
subj = null;
cancelAll(args);
}
});
})(i);
}
return subj;
};
return Future.extends(base,
{
/**
* 非同期値をコールバック関数を通して取得します。
* @param f: Either<Error,T> => any
*/
get: function (head)
{
if(this.isCompleted()) {
setTimeout(head, 0, this.done);
return;
}
var tail = this.cons;
this.cons = function (e) { head(e), tail(e) };
},
/**
* @param U
* @param f: Either<Error,T> => AsyncEither<Error,U>
* @return Future<U>
*/
map: function (f)
{
var that = this;
var subj = new Subject(function () {
that.cancel();
});
this.wait(function (e) { (that = f(e));
that.wait(function (e) {
subj._dispatch(e, Future.canceled === e.lhs().getOr(null));
});
});
return subj;
},
/**
* @param f: Either<Error,T> => void
*/
each: function (f)
{
return this.map(function (e) { return (f(e), e) });
},
/**
* @override AsyncEither<Error,T>
*/
lhs: function () { return new AsyncEitherProjection(this, "lhs") },
/**
* @override AsyncEither<Error,T>
*/
rhs: function () { return new AsyncEitherProjection(this, "rhs") },
/*
* @override AsyncEither<Error,T>
*/
dir: function () { return this.sync().dir() },
/**
* @override FutureLike<Either<Error,T>>
*/
sync: function ()
{
if (!this.isCompleted()) {
throw new Error("This task is running!");
}
return this.done;
},
/**
* @override FutureLike<Either<Error,T>>
*/
isCompleted: function () { return this.done !== null },
/**
* @override FutureLike<Either<Error,T>>
*/
wait: function (f) { this.get((function (_) { f(this.done) }).bind(this)) },
/**
* @override FutureLike<Either<Error,T>>
*/
cancel: function ()
{
if (this.disp != -1 && this._queryCancel()) {
this._dispatch(Either.lhs(Future.canceled), true);
}
},
/**
* @protected
*/
_queryCancel: function () { return true },
/**
* @protected
*/
_dispatch: function (e, interrupt)
{
if(!((e = e.sync()) instanceof Either)) {
throw new Error("bug? (*_*)");
}
if (this.disp != -1 && (!this.disp || interrupt)) {
this.disp++;
setTimeout((function (disp) {
if (this.disp == disp) {
this.disp = -1;
this.cons(e);
this.cons = null;
}
}).bind(this), 0, this.disp);
}
this.done = e;
},
/**
* @return String
*/
toString: function () { return "Future("+ (this.done || "?") +")" }
});
})(AsyncEither));
/**
* ユーザー定義の{@link Future}を作成するためのインターフェイスです。
* @super Future<T>
* @param T
*/
this["Subject"] || (
this["Subject"] = (function (base) {
function Subject(canceler)
{
base.call(this);
if (canceler) {
this._queryCancel = canceler;
}
}
return Subject.extends(base,
{
/**
* @param e: T
*/
notifyFail: function (e) { this.emit(Either.lhs(e)) },
/**
* @param e: Error
*/
notifyDone: function (e) { this.emit(Either.rhs(e)) },
/**
* @param e: Either<Error,T>
*/
emit: function (e) { this._dispatch(e) }
});
})(Future));
/**
* @param T
* @param f: => T
* @return AsyncEither<Error,T>
*/
this["Try"] = function (f)
{
try {
return Either.rhs(f());
} catch(e) {
return Either.lhs((e));
}
};
/**
* @param T
* @param X..
* @param f: ((X..) => any) => any
* @return ((X..) => T) => Future<T>
*/
this["Cps"] = function (f)
{
return (function (g)
{
var subj = new Subject();
f(function () {
subj.emit(Try(g.bind.apply(g, [null].concat
(Array.prototype.slice.call(arguments)))))
});
return subj;
})
};
}).call(this);
<html>
<head>
<title>promise.js</title>
<script src="promise.js"></script>
<script>
chain1();
chain2();
chain3();
tuple();
tupleAndCancel();
function chain1()
{
var a = new Subject();
var b = new Subject();
var c = new Subject();
a
.lhs().each(function (e) { console.log("OMG:"+ e) })
.rhs().flatMap(function (e)
{
console.log("flatMap-1:"+ e);
return b;
})
.lhs().each(function (e) { console.log("omg:"+ e) })
.rhs().flatMap(function (e)
{
console.log("flatMap-2:"+ e);
return c;
})
.lhs().each(function (e) { console.log("err:"+ e) })
.rhs().each(function (e) { console.log("res:"+ e) });
a.notifyDone("AAA");
b.notifyDone("BBB");
c.notifyDone("CCC");
}
function chain2()
{
var a = new Subject();
var b = new Subject();
var c = new Subject();
a
.lhs().each(function (e) { console.log("OMG:"+ e) })
.rhs().flatMap(function (e)
{
console.log("flatMap-1:"+ e);
return b;
})
.lhs().each(function (e) { console.log("omg:"+ e) })
.rhs().flatMap(function (e)
{
console.log("flatMap-2:"+ e);
return c;
})
.lhs().each(function (e) { console.log("err:"+ e) })
.rhs().each(function (e) { console.log("res:"+ e) });
a.notifyDone("AAA");
b.notifyFail(new Error("BBB"));
}
function chain3()
{
var a = new Subject();
var b = new Subject();
// recover
a
.lhs().flatMap(function (e)
{
console.log("error!? let's recover..:"+ e.message);
setTimeout(function ()
{
b.notifyDone("Yeah");
}, 100);
return b;
})
.rhs().each(function (e)
{
console.log("finished! "+ e);
});
a.notifyFail(new Error("OMG"));
}
function tuple()
{
var x = new Subject();
var y = new Subject();
var z = new Subject();
setTimeout(function () { x.notifyDone("XXX") }, 300);
setTimeout(function () { y.notifyDone("YYY") }, 999);
setTimeout(function () { z.notifyDone("ZZZ") }, 200);
Future.tuple(x, y, z)
.rhs().each(function (e)
{
console.log("tuple -> "+ e);
});
}
function tupleAndCancel()
{
var x = new Subject();
var y = new Subject();
var z = new Subject();
setTimeout(function () { x.notifyFail(new Error("xxx")) }, 100);
setTimeout(function () { y.notifyDone("YYY") }, 999);
setTimeout(function () { z.notifyDone("ZZZ") }, 200);
Future.tuple(x, y, z)
.rhs().each(function (e) { console.log("tuple -> "+ e) })
.lhs().each(function (e) { console.log("error -> "+ e) });
x.lhs().each(function (e) { console.log("x error: "+ e) });
y.lhs().each(function (e) { console.log("y error: "+ e) });
z.lhs().each(function (e) { console.log("z error: "+ e) });
}
</script>
</head>
<body>
<h1>promise.js</h1>
<div>
<a href="https://gist.github.com/hisui/4712504">gist</a>
</div>
</body>
</html>
@hisui
Copy link
Author

hisui commented Feb 5, 2013

Eitherかますのは冗長やろか・・・?

@hisui
Copy link
Author

hisui commented Apr 4, 2013

AsyncEither#lhs, rhsはAsyncOptionを継承するAsyncProjectionL, AsyncProjectionRを返すようにした方が良いかも。。

@hisui
Copy link
Author

hisui commented Apr 4, 2013

AsyncProjection#bindに渡す関数はAsyncEither返すようにしたほうがいい

@hisui
Copy link
Author

hisui commented Apr 4, 2013

fusion: (Future<A>, Future<B>) => Future<(A,B)>
こういうの作る予定

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