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

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,此问题的解决方案将分为以下几类:

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

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


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使用的方法,它完全避免了延迟评估标记的需要.在这种方法中,作为函数定义的一部分,可以指定任何一个非标准评估的形式参数.从字面上看,这种方法肯定是最不引人注目的,但对我来说有点太过分了.此外,它不像使用例如\延迟评估标记那样灵活,恕我直言.

Aad*_*hah 5

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

  1. 从懒惰函数返回.
  2. 或者通过编写功能创建.

从惰性函数返回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)

现在你可以一起使用repeattake如下:

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

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

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.