3

我正在移植一些严重依赖延迟评估的 Python 代码。这是通过 via thunks完成的。更具体地说,任何<expr>需要延迟评估的 Python 表达式都包含在 Python“lambda 表达式”中,即lambda:<expr>.

AFAIK,与此最接近的 JavaScript 等价物是function(){return <expr>}.

由于我正在使用的代码绝对充斥着这样的 thunk,如果可能的话,我想让它们的代码更简洁。这样做的原因不仅是为了节省字符(对于 JS 来说这是一个不可忽略的考虑因素),也是为了使代码更具可读性。要明白我的意思,比较这个标准的 JavaScript 表单:

function(){return fetchx()}

\fetchx()

在第一种形式中,实质性信息,即表达式fetchx(),在印刷上被周围的function(){return......掩盖了}。在第二种形式1中,只有一个 ( \) 字符用作“延迟评估标记”。我认为这是最佳方法2

AFAICT,这个问题的解决方案将分为以下几类:

  1. 用于eval模拟延迟评估。
  2. 一些我不知道的特殊 JavaScript 语法,它完成了我想要的。(我对 JavaScript 的极大无知使这种可能性对我来说看起来很真实。)
  3. 用一些非标准的 JavaScript 编写代码,这些代码会以编程方式处理成正确的 JavaScript。(当然,这种方法不会减少最终代码的占用空间,但至少可以保留一些可读性。)
  4. 以上都不是。

我对听到最后三类的反应特别感兴趣。


PS:我知道eval(上面的选项 1)的使用在 JS 世界中被广泛弃用,但是,FWIW,下面我给出了这个选项的玩具说明。

这个想法是定义一个私有包装类,其唯一目的是将纯字符串标记为 JavaScript 代码以进行延迟评估。然后使用具有短名称(例如C,用于“CODE”)的工厂方法来减少,例如,

function(){return fetchx()}

C('fetchx()')

首先,工厂C和辅助函数的定义maybe_eval

var C = (function () {
  function _delayed_eval(code) { this.code = code; }
  _delayed_eval.prototype.val = function () { return eval(this.code) };
  return function (code) { return new _delayed_eval(code) };
})();

var maybe_eval = (function () {
  var _delayed_eval = C("").constructor;
  return function (x) {
    return x instanceof _delayed_eval ? x.val() : x;
  }  
})();

下面的get函数和lazyget函数之间的比较显示了如何使用上述函数。

这两个函数都接受三个参数:一个对象obj、一个键key和一个默认值,obj[key]如果key存在于 中,它们都应该返回obj,否则返回默认值。

这两个函数之间的唯一区别是 for 的默认值lazyget可以是一个 thunk,如果是这样,它只会在keyis not in时被评估obj

function get(obj, key, dflt) {
  return obj.hasOwnProperty(key) ? obj[key] : dflt;
}

function lazyget(obj, key, lazydflt) {
  return obj.hasOwnProperty(key) ? obj[key] : maybe_eval(lazydflt);
}

看看这两个函数的实际作用,定义:

function slow_foo() {
  ++slow_foo.times_called;
  return "sorry for the wait!";
}
slow_foo.times_called = 0;

var someobj = {x: "quick!"};

然后,在评估上述内容后,并使用(例如)Firefox + Firebug,以下

console.log(slow_foo.times_called)              // 0

console.log(get(someobj, "x", slow_foo()));     // quick!
console.log(slow_foo.times_called)              // 1

console.log(lazyget(someobj, "x",
            C("slow_foo().toUpperCase()")));    // quick!
console.log(slow_foo.times_called)              // 1

console.log(lazyget(someobj, "y",
            C("slow_foo().toUpperCase()")));    // SORRY FOR THE WAIT!
console.log(slow_foo.times_called)              // 2

console.log(lazyget(someobj, "y",
            "slow_foo().toUpperCase()"));       // slow_foo().toUpperCase()
console.log(slow_foo.times_called)              // 2

打印出来

0
quick!
1
quick!
1
SORRY FOR THE WAIT!
2
slow_foo().toUpperCase()
2

1 ...这可能会让 Haskell 程序员感到奇怪的熟悉。:)

2还有另一种方法,例如 Mathematica 使用的方法,它完全避免了对延迟评估标记的需要。在这种方法中,作为函数定义的一部分,可以指定其任何一个形式参数用于非标准评估。排版上,这种方法当然是最大限度地不引人注目,但对我的口味来说有点太多了。此外,恕我直言,它不像使用例如\延迟评估标记那样灵活。

4

2 回答 2

5

以我的拙见,我认为您从错误的角度看待这个问题。如果您要手动创建 thunk,那么您需要考虑重构您的代码。在大多数情况下,thunk 应该是:

  1. 从惰性函数返回。
  2. 或通过组合函数创建。

从惰性函数返回 Thunk

当我第一次开始在 JavaScript 中练习函数式编程时,我被Y 组合器迷住了。根据我在网上阅读的内容,Y 组合子是一个值得崇拜的神圣实体。它以某种方式允许不知道自己名字的函数调用自己。因此,它是递归的数学表现——函数式编程最重要的支柱之一。

然而,理解 Y 组合器并非易事。Mike Vanier写道,Y 组合器的知识是“功能性识字”和不识字的人之间的跳水线。老实说,Y 组合器本身很容易理解。但是,大多数在线文章都将其向后解释,使其难以理解。例如 Wikipedia 将 Y 组合器定义为:

Y = λf.(λx.f (x x)) (λx.f (x x))

在 JavaScript 中,这将转换为:

function Y(f) {
    return (function (x) {
        return f(x(x));
    }(function (x) {
        return f(x(x));
    }));
}

Y 组合器的这个定义是不直观的,并且它并不清楚 Y 组合器是如何体现递归的。更不用说它根本不能在像 JavaScript 这样的急切语言中使用,因为表达式x(x)会立即计算,从而导致无限循环,最终导致堆栈溢出。因此,在像 JavaScript 这样的热切语言中,我们使用 Z 组合器来代替:

Z = λf.(λx.f (λv.((x x) v))) (λx.f (λv.((x x) v)))

JavaScript 中生成的代码更加混乱和不直观:

function Z(f) {
    return (function (x) {
        return f(function (v) {
            return x(x)(v);
        });
    }(function (x) {
        return f(function (v) {
            return x(x)(v);
        });
    }));
}

我们可以看到,Y 组合器和 Z 组合器之间的唯一区别是惰性表达式x(x)被急切表达式取代function (v) { return x(x)(v); }。它被包裹在一个thunk中。然而,在 JavaScript 中,将 thunk 编写如下更有意义:

function () {
    return x(x).apply(this, arguments);
}

当然,这里我们假设x(x)计算为一个函数。在 Y 组合器的情况下,这确实是正确的。但是,如果 thunk 不计算为函数,那么我们只需返回表达式。


作为一名程序员,对我来说最顿悟的时刻之一就是 Y 组合器本身就是递归的。例如在 Haskell 中,你定义 Y 组合器如下:

y f = f (y f)

因为 Haskell 是一种惰性语言,只有在需要时才对y finf (y f)进行评估,因此您不会陷入无限循环。在内部,Haskell 为每个表达式创建一个 thunk。然而,在 JavaScript 中,您需要显式创建一个 thunk:

function y(f) {
    return function () {
        return f(y(f)).apply(this, arguments);
    };
}

当然,递归地定义 Y 组合器是作弊:您只是在 Y 组合器内部显式递归。从数学上讲,Y 组合器本身应该被定义为非递归来描述递归的结构。尽管如此,无论如何我们都喜欢它。重要的是 JavaScript 中的 Y 组合器现在返回一个 thunk(即我们使用惰性语义定义它)。


为了巩固我们的理解,让我们在 JavaScript 中创建另一个惰性函数。让我们repeat在 JavaScript 中实现 Haskell 中的函数。在 Haskell 中,repeat函数定义如下:

repeat :: a -> [a]
repeat x = x : repeat x

如您所见repeat,没有边缘情况,它递归地调用自己。如果 Haskell 不是那么懒惰,它将永远递归。如果 JavaScript 是惰性的,那么我们可以实现repeat如下:

function repeat(x) {
    return [x, repeat(x)];
}

不幸的是,如果执行上述代码将永远递归,直到导致堆栈溢出。为了解决这个问题,我们返回一个 thunk 代替:

function repeat(x) {
    return function () {
        return [x, repeat(x)];
    };
}

当然,由于 thunk 不会评估为函数,我们需要另一种方法来同等对待 thunk 和正常值。因此,我们创建一个函数来评估一个 thunk,如下所示:

function evaluate(thunk) {
    return typeof thunk === "function" ? thunk() : thunk;
}

evaluate函数现在可用于实现可以将惰性或严格数据结构作为参数的函数。例如,我们可以take使用 Haskell 实现该功能evaluate。在 Haskelltake中定义如下:

take :: Int -> [a] -> [a]
take 0 _      = []
take _ []     = []
take n (x:xs) = x : take (n - 1) xs

在 JavaScript 中,我们将take使用evaluate如下实现:

function take(n, list) {
    if (n) {
        var xxs = evaluate(list);
        return xxs.length ? [xxs[0], take(n - 1, xxs[1])] : [];
    } else return [];
}

现在您可以将repeatandtake一起使用,如下所示:

take(3, repeat('x'));

亲自查看演示:

alert(JSON.stringify(take(3, repeat('x'))));

function take(n, list) {
    if (n) {
        var xxs = evaluate(list);
        return xxs.length ? [xxs[0], take(n - 1, xxs[1])] : [];
    } else return [];
}

function evaluate(thunk) {
    return typeof thunk === "function" ? thunk() : thunk;
}

function repeat(x) {
    return function () {
        return [x, repeat(x)];
    };
}

工作中的懒惰评价。


在我看来,大多数 thunk 应该是惰性函数返回的那些。您永远不必手动创建 thunk。但是,每次创建惰性函数时,您仍然需要在其中手动创建一个 thunk。这个问题可以通过提升惰性函数来解决,如下所示:

function lazy(f) {
    return function () {
        var g = f, self = this, args = arguments;

        return function () {
            var data = g.apply(self, args);
            return typeof data === "function" ?
                data.apply(this, arguments) : data;
        };
    };
}

使用该lazy函数,您现在可以定义 Y 组合器,repeat如下所示:

var y = lazy(function (f) {
    return f(y(f));
});

var repeat = lazy(function (x) {
    return [x, repeat(x)];
});

这使得 JavaScript 中的函数式编程几乎与 Haskell 或 OCaml 中的函数式编程一样有趣。查看更新的演示:

var repeat = lazy(function (x) {
    return [x, repeat(x)];
});

alert(JSON.stringify(take(3, repeat('x'))));

function take(n, list) {
    if (n) {
        var xxs = evaluate(list);
        return xxs.length ? [xxs[0], take(n - 1, xxs[1])] : [];
    } else return [];
}

function evaluate(thunk) {
    return typeof thunk === "function" ? thunk() : thunk;
}

function lazy(f) {
    return function () {
        var g = f, self = this, args = arguments;

        return function () {
            var data = g.apply(self, args);
            return typeof data === "function" ?
                data.apply(this, arguments) : data;
        };
    };
}

通过组合函数创建 Thunks

有时您需要将表达式传递给延迟评估的函数。在这种情况下,您需要创建自定义 thunk。因此我们无法使用该lazy功能。在这种情况下,您可以使用函数组合作为手动创建 thunk 的可行替代方案。函数组合在 Haskell 中定义如下:

(.) :: (b -> c) -> (a -> b) -> a -> c
f . g = \x -> f (g x)

在 JavaScript 中,这转换为:

function compose(f, g) {
    return function (x) {
        return f(g(x));
    };
}

但是,将其写为:

function compose(f, g) {
    return function () {
        return f(g.apply(this, arguments));
    };
}

数学中的函数组合从右到左读取。然而,JavaScript 中的评估总是从左到右。例如,在表达式slow_foo().toUpperCase()中,首先执行函数slow_foo,然后根据toUpperCase返回值调用方法。因此,我们希望以相反的顺序组合函数并将它们链接如下:

Function.prototype.pipe = function (f) {
    var g = this;

    return function () {
        return f(g.apply(this, arguments));
    };
};

使用该pipe方法,我们现在可以按如下方式组合函数:

var toUpperCase = "".toUpperCase;
slow_foo.pipe(toUpperCase);

上面的代码将等效于以下 thunk:

function () {
    return toUpperCase(slow_foo.apply(this, arguments));
}

然而有一个问题。该toUpperCase函数实际上是一个方法。因此返回的值slow_foo应该设置 的this指针toUpperCase。简而言之,我们希望将slow_foointo的输出通过管道传输toUpperCase如下:

function () {
    return slow_foo.apply(this, arguments).toUpperCase();
}

解决方案其实很简单,我们根本不需要修改我们的pipe方法:

var bind = Function.bind;
var call = Function.call;

var bindable = bind.bind(bind); // bindable(f) === f.bind
var callable = bindable(call);  // callable(f) === f.call

使用该callable方法,我们现在可以重构我们的代码,如下所示:

var toUpperCase = "".toUpperCase;
slow_foo.pipe(callable(toUpperCase));

因为callable(toUpperCase)相当于toUpperCase.call我们的 thunk 现在是:

function () {
    return toUpperCase.call(slow_foo.apply(this, arguments));
}

这正是我们想要的。因此我们的最终代码如下:

var bind = Function.bind;
var call = Function.call;

var bindable = bind.bind(bind); // bindable(f) === f.bind
var callable = bindable(call);  // callable(f) === f.call

var someobj = {x: "Quick."};

slow_foo.times_called = 0;

Function.prototype.pipe = function (f) {
    var g = this;

    return function () {
        return f(g.apply(this, arguments));
    };
};

function lazyget(obj, key, lazydflt) {
    return obj.hasOwnProperty(key) ? obj[key] : evaluate(lazydflt);
}

function slow_foo() {
    slow_foo.times_called++;
    return "Sorry for keeping you waiting.";
}

function evaluate(thunk) {
    return typeof thunk === "function" ? thunk() : thunk;
}

然后我们定义测试用例:

console.log(slow_foo.times_called);
console.log(lazyget(someobj, "x", slow_foo()));

console.log(slow_foo.times_called);
console.log(lazyget(someobj, "x", slow_foo.pipe(callable("".toUpperCase))));

console.log(slow_foo.times_called);
console.log(lazyget(someobj, "y", slow_foo.pipe(callable("".toUpperCase))));

console.log(slow_foo.times_called);
console.log(lazyget(someobj, "y", "slow_foo().toUpperCase()"));

console.log(slow_foo.times_called);

结果和预期的一样:

0
Quick.
1
Quick.
1
SORRY FOR KEEPING YOU WAITING.
2
slow_foo().toUpperCase()
2

因此,正如您所见,在大多数情况下,您永远不需要手动创建 thunk。使用函数提升函数lazy以使其返回 thunk 或组合函数以创建新的 thunk。

于 2013-11-08T15:34:04.823 回答
-2

如果您想要延迟执行,您应该考虑使用setTimeout.

setTimeout(function() {
    console.log("I'm delayed");
}, 10);

console.log("I'm not delayed");


>I'm not delayed

>I'm delayed

https://developer.mozilla.org/en-US/docs/Web/API/window.setTimeout

于 2013-08-30T21:30:49.710 回答