比函数(){return x}更简洁的延迟评估?

时间:2013-08-30 20:51:21

标签: javascript lazy-evaluation thunk

我正在移植一些很大程度上依赖于延迟评估的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,此问题的解决方案将分为以下几类:

  1. 使用eval模拟延迟评估。
  2. 一些我不了解的特殊JavaScript语法,它完成了我想要的。 (我对JavaScript的无知使得这种可能性对我来说非常真实。)
  3. 将代码编写在一些非标准的JavaScript中,这些JavaScript以编程方式处理成正确的JavaScript。 (当然,这种方法不会减少最终代码的占用空间,但至少可以在可读性方面保留一些增益。)
  4. 以上都不是。
  5. 我对听到最后三个类别的回答特别感兴趣。


    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那样灵活。

2 个答案:

答案 0 :(得分:5)

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

  1. 从懒惰函数返回。
  2. 或通过撰写功能创建。
  3. 从延迟函数返回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 [];
    }
    

    现在,您可以按照以下方式一起使用repeattake

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

    自己看演示:

    &#13;
    &#13;
    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;
    &#13;
    &#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中的函数式编程一样有趣。请参阅更新的演示:

    &#13;
    &#13;
    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;
    &#13;
    &#13;

    通过编写函数创建Thunk

    有时您需要将表达式传递给懒惰计算的函数。在这种情况下,您需要创建自定义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