我正在移植一些很大程度上依赖于延迟评估的Python代码。这是通过via thunks完成的。更具体地说,任何需要延迟评估的Python表达式<expr>
都包含在Python“lambda表达式”中,即lambda:<expr>
。
AFAIK,与此最接近的JavaScript是function(){return <expr>}
。
由于我正在使用的代码绝对充斥着这样的风暴,我想让它们的代码更简洁,如果可能的话。这样做的原因不仅在于保存字符(当涉及到JS时不可忽略的考虑因素),而且还使代码更具可读性。要了解我的意思,请比较此标准JavaScript形式:
function(){return fetchx()}
与
\fetchx()
在第一种形式中,实质性信息,即表达式fetchx()
,在字面上被周围function(){return
... }
遮挡。在第二种形式 1 中,只有一个(\
)字符用作“延迟评估标记”。我认为这是最佳方法 2 。
AFAICT,此问题的解决方案将分为以下几类:
eval
模拟延迟评估。我对听到最后三个类别的回答特别感兴趣。
P.S。:我知道使用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
{1}},否则为默认值。
这两个函数之间的唯一区别是lazyget
的默认值可能是thunk,如果是,只有key
不在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使用的方法,它完全避免了对延迟评估标记的需要。在这种方法中,作为函数定义的一部分,可以指定任何一个非标准评估的形式参数。从字面上看,这种方法肯定是最不引人注目的,但对我来说有点太过分了。此外,使用例如\
作为延迟评估标记并不像IMHO那样灵活。
子>
答案 0 :(得分:5)
以我的拙见,我认为你从错误的角度看待这个问题。如果您手动创建thunk,则需要考虑重构代码。在大多数情况下,thunk应该是:
当我第一次开始在JavaScript中练习函数式编程时,我被Y combinator迷惑了。根据我在网上看到的,Y组合者是一个被崇拜的神圣实体。它以某种方式允许不知道自己名字的功能自称。因此,这是递归的数学表现 - 函数式编程最重要的支柱之一。
然而,了解Y组合子并非易事。迈克·范尼尔wrote认为Y组合者的知识是那些具有“功能识字能力”的人之间的潜水线。和那些不是。老实说,Y组合器本身很容易理解。然而,大多数在线文章向后解释它使其难以理解。例如,维基百科将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 f
中的f (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中创建另一个懒惰函数。让我们在JavaScript中实现Haskell的repeat
函数。在Haskell中,repeat
函数定义如下:
repeat :: a -> [a]
repeat x = x : repeat x
正如您所看到的,repeat
没有边缘情况,并且它以递归方式调用自身。如果哈斯克尔不是那么懒惰,那么它会永远地逃脱。如果JavaScript很懒,那么我们可以按如下方式实现repeat
:
function repeat(x) {
return [x, repeat(x)];
}
不幸的是,如果执行上面的代码会永远递归,直到它导致堆栈溢出。为了解决这个问题,我们返回了一个thunk:
function repeat(x) {
return function () {
return [x, repeat(x)];
};
}
当然,由于thunk没有评估函数,我们需要另一种方法来相同地处理thunk和normal值。因此,我们创建一个函数来评估thunk如下:
function evaluate(thunk) {
return typeof thunk === "function" ? thunk() : thunk;
}
现在可以使用evaluate
函数来实现可以将惰性或严格数据结构作为参数的函数。例如,我们可以使用take
从Haskell实现evaluate
函数。在Haskell take
定义如下:
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 [];
}
现在,您可以按照以下方式一起使用repeat
和take
:
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)];
};
}
&#13;
工作中的懒惰评估。
在我的拙见中,大多数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;
};
};
}
&#13;
有时您需要将表达式传递给懒惰计算的函数。在这种情况下,您需要创建自定义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_foo
的输出管道输入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。
答案 1 :(得分:-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