【问题标题】:Fulfilling all function arguments in order before invoking在调用之前按顺序完成所有函数参数
【发布时间】:2013-06-28 17:04:48
【问题描述】:

我正在尝试了解如何使以下 zip 函数(尤其是调用函数)更具功能性。我遇到的问题是调用方法必须等待左侧和右侧都被填充,然后才能分派值。必须按顺序调用这些值,以便压缩正确的值,否则我会考虑使用 curry/partial 函数来实现这一点。

有什么我可以使用的东西可以消除这种障碍。

function zip(state, a, b) {
    var left = [];
    var right = [];

    function invoke() {
        if (left.length > 0 && right.length > 0) {
            state([left.shift(), right.shift()]);
        }
    }

    a.foreach(function(v) {
        left.push(v);
        invoke();
    });

    b.foreach(function(v) {
        right.push(v);
        invoke();
    });
}

Bellow 是满足 zip 功能的简单示例。

function Stream() {
    var env = this;
    env.subs = [];
    env.id = setInterval(function() {
        env.subs.forEach(function(f) {
            f(Math.random()); 
        });
    }, ((Math.random() * 100) + 500) | 0);
}
Stream.prototype.foreach = function(f) {
    this.subs.push(f);
}
zip(function(v) {
    console.log(v);
}, new Stream(), new Stream());

奖励:删除可变数组。

【问题讨论】:

  • 所以ab 将拥有带有异步回调的forEach 方法,这就是我们所知道的吗?还是应该让zip 更通用?
  • 我认为你想要shift 方法而不是unshift 方法。 unshift 方法将元素添加到数组的开头。 shift 方法删除数组开头的第一个元素。
  • 确实,实际的实现确实是zip(new Stream(), new Stream()).forEach(function(v) { console.log(v); });,但对于实际问题,这个细节只是关于如何让这个实际的 zip 函数运行。
  • 我还修复了 shift 与 unshift (当我输入问题时出现错字 - 好地方!)。 @Bergi 它确实是关于异步回调,我实际上想知道是否有一些来自 Haskell(etc) 或类别理论的东西我可以实现合并或共享状态。
  • 另一个不相关的实现错误:您的 Stream 为每个订阅者提供不同的随机数

标签: javascript functional-programming frp


【解决方案1】:

如果Stream 有某种iterator 接口,将列表划分为第一个元素及其后继元素(就像构建了Haskell 列表,你似乎知道它们),则可能会有更实用的方法。

我知道一开始这段代码更复杂(至少更长),但使用结构更方便:

function Promise(resolver) {
    // you know better promise libs of course
    // this one is not even monadic
    var subs = [],
        res = null;
    resolver(function resolve() {
        res = arguments;
        while (subs.length) subs.shift().apply(null, res);
    });
    this.onData = function(f) {
        if (res)
            f.apply(null, res);
        else
            subs.push(f);
        return this;
    };
}
Promise.all = function() {
    var ps = Array.prototype.concat.apply([], arguments);
    return new Promise(function(resolve) {
        var res = [],
            l = ps.length;
        ps.forEach(function(p, i) {
            p.onData(function() {
                while(res.length < arguments.length) res.push([]);
                for (var j=0; j<arguments.length; j++)
                    res[j][i] = arguments[j];
                if (--l == 0)
                    resolve.apply(null, res);
            });
        });
    });
};
function Stream() {
    // an asynchronous (random) list
    var that = this,
        interval = (Math.random() * 100 + 500) | 0;
    this.first = new Promise(function create(resolve) {
        that.id = setTimeout(function() {
            resolve(Math.random(), new Promise(create));
        }, interval);
    });
}
// this is how to consume a stream:
Stream.prototype.forEach = function(f) {
    this.first.onData(function fire(res, next) {
        f(res);
        next.onData(fire);
    });
    return this;
};
Stream.prototype.end = function() { clearTimeout(this.id); return this; };

但是现在压缩它们很容易:

function zip() {
    var res = Object.create(Stream.prototype); // inherit the Stream interface
    res.first = (function create(firsts) {
        return new Promise(function(resolve) {
            Promise.all(firsts).onData(function(results, nexts) {
                resolve(results, create(nexts));
            });
        });
    })(Array.prototype.map.call(arguments, function(stream) {
        return stream.first;
    }));
    return res;
}
zip(new Stream, new Stream).forEach(console.log.bind(console));

基本上,我已将您对第一个项目的等待概括为 Promise 模式,其中Promise.all 具有并行等待功能,并且您的可变结果数组为嵌套的承诺列表。而且我通过使所有函数使用任意数量的参数来避免代码重复(对于leftright)。

【讨论】:

  • Indeed Promises 就是答案。我的实现方式略有不同,但这是一个非常好的见解。再次感谢。
【解决方案2】:

我不明白你为什么要让你的代码更实用。尽管如此,我确实通过完全删除 invoke 函数来改进您的 zip 函数。你真的不需要它:

function zip(a, b, callback) {
    var left = [], right = [];

    a.forEach(function (value) {
        if (right.length)
            callback([value, right.shift()]);
        else left.push(value);
    });

    b.forEach(function (value) {
        if (left.length)
            callback([left.shift(), value]);
        else right.push(value);
    });
}

自己查看输出:http://jsfiddle.net/Tw6K2/

代码比功能更重要。但是我怀疑它会比这更好。

【讨论】:

  • >但是我怀疑它会比这更好这是我的想法。我想知道是否可以使用 StateMonad 或类似的东西来帮助处理可变的左/右数组。
【解决方案3】:

在对您的问题进行了更多思考之后,我相信我找到了更通用的解决方案。让我们从EventStream 构造函数开始(它比你的Stream 构造函数更通用):

function EventStream() {
    this.listeners = [];
}

然后我们创建一个dispatch 方法来将事件添加到流中:

EventStream.prototype.dispatch = function (event) {
    return this.listeners.map(function (listener) {
        return listener(event);
    });
};

接下来我们将创建一个map 方法,它再次比您的foreach 方法更通用:

EventStream.prototype.map = function (f) {
    var stream = new EventStream;

    this.listeners.push(function (x) {
        return stream.dispatch(f(x));
    });

    return stream;
};

现在,当您在事件流上map 一个函数时,您将获得一个全新的事件流。例如,如果您的流是[0,1,3,5..],并且您将(+2) 映射到它上面,那么新流将是[2,3,5,7..]

我们还将创建一些更有益的实用方法,例如 filterscanmerge,如下所示:

EventStream.prototype.filter = function (f) {
    var stream = new EventStream;

    this.listeners.push(function (x) {
        if (f(x)) return stream.dispatch(x);
    });

    return stream;
};

filter 方法过滤掉事件流中的某些事件以创建一个全新的事件流。例如,给定[2,3,5,7..] 和函数odd,过滤后的事件流将是[3,5,7..]

EventStream.prototype.scan = function (a, f) {
    var stream = new EventStream;

    setTimeout(function () {
        stream.dispatch(a);
    });

    this.listeners.push(function (x) {
        return stream.dispatch(a = f(a, x));
    });

    return stream;
};

scan 方法用于累积创建新的事件流。例如给定流[3,5,7..],初始值0和扫描函数(+),新的事件流将是[0,3,8,15..]

EventStream.prototype.merge = function (that) {
    var stream = new EventStream;

    this.listeners.push(function (x) {
        return stream.dispatch(new Left(x));
    });

    this.listeners.push(function (y) {
        return stream.dispatch(new Right(x));
    });

    return stream;
};

function Left(x) {
    this.left = x;
}

function Right(x) {
    this.right = x;
}

merge 方法将两个独立的事件流合二为一。为了区分哪个流生成了每个事件,我们将所有事件标记为左或右。


好的,现在解决更大的问题。让我们创建一个zip 方法。真正酷的是我们可以使用mapfilterscanmerge 方法创建zip,如下所示:

EventStream.prototype.zip = function (that) {
    return this.merge(that).scan([[], [], null], function (acc, event) {
        var left = acc[0], right = acc[1];

        if (event instanceof Left) {
            var value = event.left;

            return right.length ?
                [left, right.slice(1), new Just([value, right[0]])] :
                [left.concat(value), right, null];
        } else {
            var value = event.right;

            return left.length ?
                [left.slice(1), right, new Just([left[0], value])] :
                [tuple(left, right.concat(value), null];
        }
    })

    .filter(function (a) {
        return a[2] instanceof Just;
    })

    .map(function (a) {
        return a[2].just;
    });
};

function Just(x) {
    this.just = x;
}

现在你可以按如下方式使用它:

stream1.zip(stream2).map(function (v) {
    console.log(v);
});

你可以定义stream1stream2如下:

var stream1 = getRandomStream();
var stream2 = getRandomStream();

function getRandomStream() {
    var stream = new EventStream;

    setInterval(function () {
        stream.dispatch(Math.random());
    }, ((Math.random() * 100) + 500) | 0);

    return stream;
}

仅此而已。不需要承诺。

【讨论】:

    猜你喜欢
    • 2021-08-02
    • 1970-01-01
    • 2015-12-03
    • 1970-01-01
    • 2019-01-10
    • 2020-02-15
    • 1970-01-01
    • 2020-04-17
    相关资源
    最近更新 更多