読者です 読者をやめる 読者になる 読者になる
無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

[JavaScript] Deferredを作ってみた

さまざまなライブラリで用意されているようなのでいまさら感満載ですが、Deferredを作ってみましたよ。

  • 諸般の事情で必要なのだけど、このためだけにjQueryに依存するのもあれなので・・・、ということで勉強もかねて再発明。
  • APIは、jQueryに合わせてみました。

詳細な仕様はこちら。実装は以下です。

define( function(){
    
    "use strict";
    
    /**
     * @interface 
     * @template <V> value type
     * @template <E> error type
     */
    var Promise = {};

    /**
     * @param {function(V):R} successCallback
     * @param {function(E):R} failCallback
     * @return {Promise.<R,*>} 
     */
    Promise.then = function( successCallback, failCallback ){};

    /**
     * @param {function((V|E)):R} callback
     * @return {Promise.<R,*>} 
     */
    Promise.always = function( callback ){};

    /**
     * @param {function(V):R} successCallback
     * @return {Promise.<R,*>} 
     */
    Promise.done = function( successCallback ){};

    /**
     * @param {function(E):R} failCallback
     * @return {Promise.<R,*>} 
     */
    Promise.fail = function( failCallback ){};

    /**
     * @template <X>
     * @template <Y>
     * @param {function(V):X} resultFilter
     * @param {function(E):Y=} errorFilter
     * @return {Promise.<X,Y>}
     */
    Promise.pipe = function( resultFilter, errorFilter ){};
    
    /**
     * @return {boolean} 
     */
    Promise.fixed = function(){};
    
    /**
     * @return {boolean} 
     */
    Promise.rejected = function(){};
    
    /**
     * @return {boolean} 
     */
    Promise.resolved = function(){};
    
    /**
     * @class 
     * @implements {Promise}
     */
    var Deferred = function(){
        this.state = states.unresolved;
        this.result = null;
        this.successCallbacks = [];
        this.failCallbacks = [];
        Object.seal( this );
    };

    /** @override */
    Deferred.prototype.then = function( successCallback, failCallback ){
        var d = new Deferred();
        this.state.done.call(this,successCallback,d);
        this.state.fail.call(this,failCallback,d);
        return d;
    };
    /** @override */
    Deferred.prototype.always = function( callback ){
        return this.then(callback, callback);
    };
    /** @override */
    Deferred.prototype.done = function( successCallback ){
        var d = new Deferred();
        this.state.done.call(this,successCallback,d);
        return d;
    };
    /** @override */
    Deferred.prototype.fail = function( failCallback ){
        var d = new Deferred();
        this.state.fail.call(this,failCallback,d);
        return d;
    };
    /**
     * @param {V} result
     * @return {Deferred.<V,E>} this 
     */
    Deferred.prototype.resolve = function( result ){
        this.state.resolve.call(this,result);
        return this;
    };
    /**
     * @param {E} error
     * @return {Deferred.<V,E>} this 
     */
    Deferred.prototype.reject = function( error ){
        this.state.reject.call(this,error);
        return this;
    };
    
    /** @override */
    Deferred.prototype.fixed = function(){
        return this.rejected() || this.resolved() ;
    };
    /** @override */
    Deferred.prototype.rejected = function(){
        return this.state === states.rejected;
    };
    /** @override */
    Deferred.prototype.resolved = function(){
        return this.state === states.resolved;
    };
    
    /**
     * @return {Promise.<V,E>}
     */
    Deferred.prototype.promise = function( ){
        return Object.seal({
            then     : this.then.bind( this ),
            done     : this.done.bind( this ),
            fail     : this.fail.bind( this ),
            always   : this.always.bind( this ),
            pipe     : this.pipe.bind( this ),
            fixed    : this.fixed.bind( this ),
            rejected : this.rejected.bind( this ),
            resolved : this.resolved.bind( this )
        });
    };
    
    /** @override */
    Deferred.prototype.pipe = function( resultFilter, errorFilter ) {
        var d = new Deferred();
        this.then( function( result ){
            try {
                d.resolve( resultFilter ? resultFilter(result) : result );
            } catch (e) {
                d.reject( errorFilter ? errorFilter(e) : e );
            }
        }, function( error ){
            try {
                error = errorFilter ? errorFilter(error) : error;
            } catch (e) {}
            d.reject( error );
        });
        return d.promise();
    };
    
    /**
     * @template <X>
     * @param {X} value
     * @return {Promise.<X,*>}
     */
    Deferred.valueOf = function( value ) {
        var d = new Deferred();
        d.resolve(value);
        return d.promise();
    };
    
    /**
     * @template <X>
     * @param {X} error
     * @return {Promise.<*,X>}
     */
    Deferred.errorOf = function( error ) {
        var d = new Deferred();
        d.reject(error);
        return d.promise();
    };
    
    /**
     * @template <X>
     * @param {Array.<Deferred.<X,*>>} deferreds
     * @return {Promise.<Array.<X>.*>}
     */
    Deferred.when = function( deferreds ) {
        var d = new Deferred();
        var results = [];
        var counter = deferreds.length-1;
        var callback = function( result ) {
            results.push(result);
            if ( --counter === 0 && !d.fixed() ) {
                d.resolve(results);
            }
        };
        var errorback = function( error ) {
            if ( !d.fixed() ) d.reject(error);
        };
        deferreds.forEach( function(deferred){
            deferred.then(callback, errorback);
        });
        return d.promise();
    };
    
    // private
    var alreadyFixed =  function() {
        throw {message: "already resolved or rejected."};
    };
    var notify = function( callback, result ){
        callback.call( null, result );
    };
    var notifyAll = function( callbacks, result ){
        var i,n;
        for ( i=0,n=callbacks.length;i<n;i++ ) {
            notify( callbacks[i], result );
        }
    };
    var chain = function( callback, deferred ){
        return function(resultOrError){
            try {
                var result = callback(resultOrError);
                deferred.resolve(result);
            } catch (er) {
                deferred.reject(er);
            }
        };
    };
    var states = {
        unresolved : {
            done : function( successCallback, deferred ) {
                this.successCallbacks.push( chain ( successCallback, deferred ) );
                return deferred;
            },
            fail : function( failCallback, deferred ) {
                this.failCallbacks.push( chain ( failCallback, deferred ) );
                return deferred;
            },
            resolve : function( result ){
                this.result = result;
                this.state = states.resolved;
                notifyAll( this.successCallbacks, result );
                this.successCallbacks = undefined;
            },
            reject: function( error ){
                this.result = error;
                this.state = states.rejected;
                notifyAll( this.failCallbacks, error );
                this.failCallbacks = undefined;
            }
        },
        resolved : {
            done : function( successCallback, deferred ) {
                chain( successCallback, deferred )(this.result);
            },
            fail : function( failCallback, deferred ) {},
            resolve : alreadyFixed,
            reject :  alreadyFixed
        },
        rejected : {
            done : function( successCallback, deferred ) {},
            fail : function( failCallback, deferred ) {
                chain( failCallback, deferred )(this.result);
            },
            resolve : alreadyFixed,
            reject :  alreadyFixed
        }
    };
    
    return Deferred;
});

追記:2011-11-03

以下の変更を行ったものに差し替えました。