【问题标题】:More succinct delayed evaluation than function(){return x}?比 function(){return x} 更简洁的延迟评估?
【发布时间】:2013-08-30 20:51:20
【问题描述】:

我正在移植一些严重依赖延迟评估的 Python 代码。这是通过thunks 完成的。更具体地说,任何需要延迟评估的 Python 表达式 <expr> 都包含在 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 会以编程方式处理成正确的 JavaScript。 (当然,这种方法不会减少最终代码的占用空间,但至少可以保留一些可读性。)
  4. 以上都不是。

我对听到最后三个类别的回答特别感兴趣。


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

这个想法是定义一个 private 包装类,其唯一目的是将纯字符串标记为 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和一个默认值,如果key存在于obj中,它们都应该返回obj[key],否则返回默认值价值。

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

【问题讨论】:

  • 看看CoffeeScript。 IMO 工厂函数的问题不是 eval,而是您失去了从创建 lambda 的范围内关闭变量的能力。
  • 我确实看过 CoffeeScript,顺便说一句,但是在看到它如何处理范围之后,特别是在阅读了作者对他的设计选择背后的想法的描述之后,我决定我不会用十英尺长的杆子。事实上,如果可以的话,我什至不会使用用 CS 编写的代码。

标签: javascript lazy-evaluation thunk


【解决方案1】:

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

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

从惰性函数返回 Thunks

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

然而,理解 Y 组合器并非易事。 Mike Vanier wrote 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 等 Eager 语言中,我们使用 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 是一种惰性语言,f (y 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 没有极端情况,它会递归调用自身。如果 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 函数现在可用于实现可以将惰性或严格数据结构作为参数的函数。例如,我们可以使用 evaluate 实现 Haskell 中的 take 函数。在 Haskell 中take 定义如下:

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

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

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'));

亲自观看演示:

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 返回的值应该设置toUpperCasethis 指针。简而言之,我们希望将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。

【讨论】:

    【解决方案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

    【讨论】:

    • OP 不想延迟表达式的执行。 OP 想要延迟表达式的 求值。因此在这种情况下setTimeout 将不起作用。我们使用setTimeout 来异步执行程序。不适用于对程序的懒惰评估。
    猜你喜欢
    • 1970-01-01
    • 2021-05-17
    • 1970-01-01
    • 2011-03-03
    • 1970-01-01
    • 2016-06-20
    • 1970-01-01
    • 2014-10-18
    • 1970-01-01
    相关资源
    最近更新 更多