kjo*_*kjo 3 javascript lazy-evaluation thunk
我正在移植一些很大程度上依赖于延迟评估的Python代码.这是通过thunks完成的.更具体地说,任何<expr>需要延迟评估的Python表达式都包含在Python"lambda表达式"中,即lambda:<expr>.
AFAIK,最接近的JavaScript等价物function(){return <expr>}.
由于我正在使用的代码绝对充斥着这样的thunk,我想让它们的代码更加简洁,如果可能的话.这样做的原因不仅在于保存字符(当涉及到JS时不可忽略的考虑因素),而且还使代码更具可读性.要了解我的意思,请比较此标准JavaScript表单:
function(){return fetchx()}
Run Code Online (Sandbox Code Playgroud)
同
\fetchx()
Run Code Online (Sandbox Code Playgroud)
在第一种形式中,实质性信息,即表达fetchx(),在印刷中被周围的function(){return...... 掩盖}.在第二种形式1中,只有一个(\)字符用作"延迟评估标记".我认为这是最佳方法2.
AFAICT,此问题的解决方案将分为以下几类:
eval模拟延迟评价.我对听到最后三个类别的回答特别感兴趣.
PS:我知道使用eval(上面的选项1)在JS世界中被广泛弃用,但是,FWIW,下面我给出了这个选项的玩具插图.
我们的想法是定义一个私有包装类,其唯一目的是将纯字符串标记为用于延迟评估的JavaScript代码.然后使用具有短名称的工厂方法(例如C,用于"CODE")来减少例如,
function(){return fetchx()}
Run Code Online (Sandbox Code Playgroud)
至
C('fetchx()')
Run Code Online (Sandbox Code Playgroud)
首先,工厂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;
}
})();
Run Code Online (Sandbox Code Playgroud)
get函数和lazyget函数之间的以下比较显示了如何使用上述函数.
这两个函数都有三个参数:一个对象obj,一个键key和一个默认值,obj[key]如果key存在,它们都应返回obj,否则应返回默认值.
这两个函数之间的唯一区别是,默认值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);
}
Run Code Online (Sandbox Code Playgroud)
在实际操作中看到这两个功能,定义:
function slow_foo() {
++slow_foo.times_called;
return "sorry for the wait!";
}
slow_foo.times_called = 0;
var someobj = {x: "quick!"};
Run Code Online (Sandbox Code Playgroud)
然后,在评估上述内容后,使用(例如)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
Run Code Online (Sandbox Code Playgroud)
打印出来
0
quick!
1
quick!
1
SORRY FOR THE WAIT!
2
slow_foo().toUpperCase()
2
Run Code Online (Sandbox Code Playgroud)
1 ...这可能会让Haskell程序员感到非常熟悉.:)
2还有另一种方法,例如Mathematica使用的方法,它完全避免了延迟评估标记的需要.在这种方法中,作为函数定义的一部分,可以指定任何一个非标准评估的形式参数.从字面上看,这种方法肯定是最不引人注目的,但对我来说有点太过分了.此外,它不像使用例如\延迟评估标记那样灵活,恕我直言.
在我的拙见中,我认为你从错误的角度看待这个问题.如果您手动创建thunk,则需要考虑重构代码.在大多数情况下,thunk应该是:
当我第一次开始用JavaScript编写函数式编程时,我被Y组合器迷惑了.根据我在网上看到的,Y组合者是一个被崇拜的神圣实体.它以某种方式允许不知道自己名字的函数自称.因此,它是递归的数学表现 - 函数式编程最重要的支柱之一.
然而,了解Y组合器并非易事.迈克·范尼尔( Mike Vanier)写道,Y组合者的知识是那些"功能上有文化"的人与那些没有"功能识字"的人之间的潜水线.老实说,Y组合器本身很容易理解.然而,大多数在线文章向后解释它使其难以理解.例如,维基百科将Y组合子定义为:
Y = ?f.(?x.f (x x)) (?x.f (x x))
Run Code Online (Sandbox Code Playgroud)
在JavaScript中,这将转换为:
function Y(f) {
return (function (x) {
return f(x(x));
}(function (x) {
return f(x(x));
}));
}
Run Code Online (Sandbox Code Playgroud)
Y组合子的这种定义是不直观的,并且它没有表明Y组合子如何是递归的表现.更不用说它不能在像JavaScript这样的热切语言中使用,因为表达式x(x)会立即被评估,从而导致无限循环,最终导致堆栈溢出.因此,在像JavaScript这样的热切语言中,我们使用Z组合器:
Z = ?f.(?x.f (?v.((x x) v))) (?x.f (?v.((x x) v)))
Run Code Online (Sandbox Code Playgroud)
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);
});
}));
}
Run Code Online (Sandbox Code Playgroud)
通常我们可以看到Y组合子和Z组合子之间的唯一区别是懒惰表达式x(x)被急切表达式所取代function (v) { return x(x)(v); }.它被包裹在一个thunk中.但是在JavaScript中,编写thunk更有意义,如下所示:
function () {
return x(x).apply(this, arguments);
}
Run Code Online (Sandbox Code Playgroud)
当然,这里我们假设x(x)评估函数.在Y组合子的情况下,这确实是正确的.但是,如果thunk不计算函数,那么我们只返回表达式.
对于我来说,作为一名程序员,最令人沮丧的时刻之一就是Y组合器本身就是递归的.例如,在Haskell中,您可以按如下方式定义Y组合器:
y f = f (y f)
Run Code Online (Sandbox Code Playgroud)
因为Haskell是一种惰性语言y f,f (y f)所以只在需要时才会对in 进行求值,因此你不会遇到无限循环.内部Haskell为每个表达式创建一个thunk.但是在JavaScript中你需要明确地创建一个thunk:
function y(f) {
return function () {
return f(y(f)).apply(this, arguments);
};
}
Run Code Online (Sandbox Code Playgroud)
当然,递归地定义Y组合器就是作弊:你只是在Y组合器中明确地递归.在数学上,Y组合器本身应该非递归地定义以描述递归的结构.尽管如此,无论如何我们都喜欢它.重要的是JavaScript中的Y组合器现在返回一个thunk(即我们使用惰性语义定义它).
为了巩固我们的理解,让我们在JavaScript中创建另一个懒惰函数.让我们repeat用JavaScript 实现Haskell中的函数.在Haskell中,repeat函数定义如下:
repeat :: a -> [a]
repeat x = x : repeat x
Run Code Online (Sandbox Code Playgroud)
你可以看到repeat没有边缘情况,它递归调用自己.如果哈斯克尔不是那么懒惰,那么它会永远地逃脱.如果JavaScript是懒惰的,那么我们可以实现repeat如下:
function repeat(x) {
return [x, repeat(x)];
}
Run Code Online (Sandbox Code Playgroud)
不幸的是,如果执行上面的代码会永远递归,直到它导致堆栈溢出.为了解决这个问题,我们返回了一个thunk:
function repeat(x) {
return function () {
return [x, repeat(x)];
};
}
Run Code Online (Sandbox Code Playgroud)
当然,因为thunk没有评估函数,我们需要另一种方法来相同地处理thunk和normal值.因此,我们创建一个函数来评估thunk如下:
function evaluate(thunk) {
return typeof thunk === "function" ? thunk() : thunk;
}
Run Code Online (Sandbox Code Playgroud)
该evaluate函数现在可用于实现可以采用惰性或严格数据结构作为参数的函数.例如,我们可以take使用Haskell 实现该函数evaluate.在Haskell take中定义如下:
take :: Int -> [a] -> [a]
take 0 _ = []
take _ [] = []
take n (x:xs) = x : take (n - 1) xs
Run Code Online (Sandbox Code Playgroud)
在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 [];
}
Run Code Online (Sandbox Code Playgroud)
现在你可以一起使用repeat和take如下:
take(3, repeat('x'));
Run Code Online (Sandbox Code Playgroud)
亲自看看演示:
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)];
};
}Run Code Online (Sandbox Code Playgroud)
在工作中懒惰的评价.
在我的拙见中,大多数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;
};
};
}
Run Code Online (Sandbox Code Playgroud)
使用该lazy功能,您现在可以定义Y组合器,repeat如下所示:
var y = lazy(function (f) {
return f(y(f));
});
var repeat = lazy(function (x) {
return [x, repeat(x)];
});
Run Code Online (Sandbox Code Playgroud)
这使得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;
};
};
}Run Code Online (Sandbox Code Playgroud)
有时您需要将表达式传递给懒惰计算的函数.在这种情况下,您需要创建自定义thunk.因此我们无法利用这个lazy功能.在这种情况下,您可以使用函数组合作为手动创建thunk的可行替代方法.函数组成在Haskell中定义如下:
(.) :: (b -> c) -> (a -> b) -> a -> c
f . g = \x -> f (g x)
Run Code Online (Sandbox Code Playgroud)
在JavaScript中,这转换为:
function compose(f, g) {
return function (x) {
return f(g(x));
};
}
Run Code Online (Sandbox Code Playgroud)
但是将它写成以下内容更有意义:
function compose(f, g) {
return function () {
return f(g.apply(this, arguments));
};
}
Run Code Online (Sandbox Code Playgroud)
数学中的函数组成从右到左阅读.但是,JavaScript中的评估始终是从左到右.例如,在表达式中首先执行slow_foo().toUpperCase()函数slow_foo,然后toUpperCase在其返回值上调用该方法.因此,我们希望以相反的顺序组合函数并将它们链接如下:
Function.prototype.pipe = function (f) {
var g = this;
return function () {
return f(g.apply(this, arguments));
};
};
Run Code Online (Sandbox Code Playgroud)
使用该pipe方法,我们现在可以编写如下函数:
var toUpperCase = "".toUpperCase;
slow_foo.pipe(toUpperCase);
Run Code Online (Sandbox Code Playgroud)
上面的代码将等同于以下thunk:
function () {
return toUpperCase(slow_foo.apply(this, arguments));
}
Run Code Online (Sandbox Code Playgroud)
但是有一个问题.该toUpperCase功能实际上是一种方法.因此返回的值slow_foo应该设置this指针toUpperCase.总之,我们要管的输出slow_foo成toUpperCase如下:
function () {
return slow_foo.apply(this, arguments).toUpperCase();
}
Run Code Online (Sandbox Code Playgroud)
解决方案实际上非常简单,我们根本不需要修改我们的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
Run Code Online (Sandbox Code Playgroud)
使用该callable方法,我们现在可以重构我们的代码,如下所示:
var toUpperCase = "".toUpperCase;
slow_foo.pipe(callable(toUpperCase));
Run Code Online (Sandbox Code Playgroud)
既然callable(toUpperCase)相当于toUpperCase.call我们的thunk现在:
function () {
return toUpperCase.call(slow_foo.apply(this, arguments));
}
Run Code Online (Sandbox Code Playgroud)
这正是我们想要的.因此,我们的最终代码如下:
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;
}
Run Code Online (Sandbox Code Playgroud)
然后我们定义测试用例:
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);
Run Code Online (Sandbox Code Playgroud)
结果如预期:
0
Quick.
1
Quick.
1
SORRY FOR KEEPING YOU WAITING.
2
slow_foo().toUpperCase()
2
Run Code Online (Sandbox Code Playgroud)
因此,在大多数情况下您可以看到,您永远不需要手动创建thunk.使用该函数提升函数lazy使它们返回thunk或组合函数以创建新的thunk.