比function(){return x}更简洁的延迟评估? [英] More succinct delayed evaluation than function(){return x}?

查看:77
本文介绍了比function(){return x}更简洁的延迟评估?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在移植一些严重依赖延迟评估的Python代码.这是通过 暴徒 .更具体地说,任何需要延迟求值的Python表达式<expr>都包含在Python"lambda表达式"中,即lambda:<expr>.

I'm porting some Python code that relies heavily on delayed evaluation. This is accomplished by via thunks. More specifically, any Python expression <expr> for which delayed evaluation is desired gets enclosed within a Python "lambda expression", i.e. lambda:<expr>.

AFAIK,与此最接近的JavaScript等效项是function(){return <expr>}.

AFAIK, the closest JavaScript equivalent of this is function(){return <expr>}.

由于我正在使用的代码在这样的杂乱中绝对是充斥的,因此,我想尽可能使它们的代码更简洁.这样做的原因不仅是为了保存字符(当涉及到JS时,这是不可忽略的考虑),而且还在于使代码更具可读性.要了解我的意思,请比较以下标准JavaScript形式:

Since the code I'm working with is absolutely awash in such thunks, I'd like to make the code for them more succinct, if at all possible. The reason for this is not only to save characters (a non-negligible consideration when it comes to JS), but also to make the code more readable. To see what I mean, compare this standard JavaScript form:

function(){return fetchx()}

使用

\fetchx()

在第一种形式中,实质性信息(即表达式fetchx())在印刷上被周围的function(){return ... }遮盖.在第二种形式 1 中,仅一个(\)字符用作延迟评估标记".我认为这是最佳方法 2 .

In the first form, the substantive information, namely the expression fetchx(), is typographically obscured by the surrounding function(){return...}. In the second form1, just one (\) character is used as "delayed evaluation marker". I think this is the optimal approach2.

AFAICT,针对此问题的解决方案可以分为以下几类:

AFAICT, solutions to this problem would fall into the following categories:

  1. 使用eval模拟延迟评估.
  2. 一些我不知道的特殊JavaScript语法,可以实现我想要的功能. (我对JavaScript的极大了解使这种可能性对我来说确实很真实.)
  3. 使用一些非标准JavaScript编写代码,这些代码会通过编程方式处理为正确的JavaScript. (当然,这种方法不会减少最终代码的占用空间,但至少可以保留一些可读性.)
  4. 以上都不是.
  1. Using eval to simulate delayed evaluation.
  2. Some special JavaScript syntax that I don't know about, and that accomplishes what I want. (My vast ignorance of JavaScript makes this possibility look quite real to me.)
  3. Writing the code in some non-standard JavaScript that gets programmatically processed into correct JavaScript. (Of course, this approach will not reduce the final code's footprint, but may at least retain some gains in readability.)
  4. None of the above.

我对听到最近三类的回复特别感兴趣.

I'm particularly interested in hearing responses of the last three categories.

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

P.S.: I'm aware that the use of eval (option 1 above) is widely deprecated in the JS world, but, FWIW, below I give a toy illustration of this option.

想法是定义一个 private 包装器类,其唯一目的是将纯字符串标记为JavaScript代码以进行延迟评估.然后使用具有短名称(例如C,代表"CODE")的工厂方法来减少,例如,

The idea is to define a private wrapper class whose sole purpose would be to tag plain strings as JavaScript code for delayed evaluation. A factory method with a short name (e.g. C, for "CODE") is then used to reduce, e.g.,

function(){return fetchx()}

C('fetchx()')

首先,定义工厂C和辅助函数maybe_eval:

First, definitions of the factory C and of the helper function 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函数之间的比较显示了如何使用上述函数.

The following comparison between a get function and a lazyget function shows how the above would be used.

两个函数都带有三个参数:对象obj,键key和默认值,并且如果obj中存在key,它们都应返回obj[key],否则,默认值值.

Both functions take three arguments: an object obj, a key key, and a default value, and they both should return obj[key] if key is present in obj, and otherwise, the default value.

这两个函数之间的唯一区别是lazyget的默认值可以是thunk,如果是,则只有key不在obj中时,它才会被求值.

The only difference between the two functions is that the default value for lazyget can be a thunk, and if so, it will get evaluated only if key is not in 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);
}

要看到这两个功能的实际作用,请定义:

Too see these two functions in action, define:

function slow_foo() {
  ++slow_foo.times_called;
  return "sorry for the wait!";
}
slow_foo.times_called = 0;

var someobj = {x: "quick!"};

然后,在对上述内容进行评估并使用Firefox + Firebug之后,执行以下操作

Then, after evaluating the above, and using (e.g.) Firefox + Firebug, the following

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程序员感到陌生. :)


1...which may strike Haskell programmers as strangely familiar. :)

2 还有另一种方法,例如由Mathematica使用的一种方法,完全避免了延迟评估标记的需要.在这种方法中,作为函数定义的一部分,可以为非标准评估指定其形式参数中的任何一个.从印刷上来说,这种方法当然是最大程度地不引人注目,但对于我的口味来说,实在是太过分了.此外,恕我直言,它不像例如使用\作为延迟评估标记那样灵活.

2There's another approach, the one used, e.g., by Mathematica, that avoids the need for delayed evaluation markers altogether. In this approach, as part of a function's definition, one can designate any one of its formal arguments for non-standard evaluation. Typographically, this approach is certainly maximally unobtrusive, but a bit too much so for my taste. Besides, it is not as flexible, IMHO, as using, e.g., \ as a delayed evaluation marker.

推荐答案

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

In my humble opinion I think you're looking at this problem from a wrong perspective. If you're creating thunks manually then you need to consider refactoring your code. In most cases thunks should be:

  1. 从惰性函数返回的任何一个.
  2. 或通过组合函数创建.

从懒惰函数返回重击

当我第一次开始使用JavaScript进行函数式编程时,我被

Returning Thunks from Lazy Functions

When I first started practicing functional programming in JavaScript I was mystified by the Y combinator. From what I had read online the Y combinator was a divine entity to be worshipped. It somehow allowed functions which didn't know their own name to call themselves. Hence it was the mathematical manifestation of recursion - one of the most important pillars of functional programming.

但是,了解Y组合器并非易事. Mike Vanier ,Y组合器的知识是那些功能强大"的人之间的跳水线识字"和那些没有识字的人.坦白地说,Y组合器本身已经死了,很容易理解.但是,大多数在线文章向后解释它,因此很难理解.例如,维基百科将Y组合器定义为:

However understanding the Y combinator was no easy feat. Mike Vanier wrote that the knowledge of the Y combinator is a diving line between those people who are "functionally literate" and those who aren't. Honestly, the Y combinator in itself is dead simple to understand. However most articles online explain it backwards making it difficult to understand. For example Wikipedia defines the Y combinator as:

Y = λf.(λx.f (x x)) (λx.f (x x))

在JavaScript中,这将转换为:

In JavaScript this would translate to:

function Y(f) {
    return (function (x) {
        return f(x(x));
    }(function (x) {
        return f(x(x));
    }));
}

Y组合器的这种定义是不直观的,也无法使Y组合器如何体现递归.更不用说它不能在像JavaScript这样的急切语言中使用,因为表达式x(x)会立即被求值,从而导致无限循环,最终导致堆栈溢出.因此,在像JavaScript这样的急切语言中,我们改用Z组合器:

This definition of the Y combinator is unintuitive and it doesn't make apparent how the Y combinator is a manifestation of recursion. Not to mention that it cannot be used at all in eager languages like JavaScript because the expression x(x) is evaluated immediately resulting in an infinite loop which eventually results in a stack overflow. Hence in eager languages like JavaScript we use the Z combinator instead:

Z = λf.(λx.f (λv.((x x) v))) (λx.f (λv.((x x) v)))

JavaScript中产生的代码更加混乱和不直观:

The resulting code in JavaScript is even more confusing and unintuitive:

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); }代替.它被包裹在一个大块的东西中.但是,在JavaScript中,编写如下代码的thunk更有意义:

Trivially we can see that the only difference between the Y combinator and the Z combinator is that the lazy expression x(x) is replaced by the eager expression function (v) { return x(x)(v); }. It is wrapped in a thunk. In JavaScript however it makes more sense to write the thunk as follows:

function () {
    return x(x).apply(this, arguments);
}

当然,这里我们假设x(x)的值是一个函数.对于Y组合器,这确实是正确的.但是,如果thunk不能求值到函数,那么我们只返回表达式.

Of course here we're assuming that x(x) evaluates to a function. In the case of the Y combinator this is indeed true. However if the thunk doesn't evaluate to a function then we simply return the expression.

对我来说,作为程序员最重要的时刻之一就是Y组合器本身就是递归的.例如,在Haskell中,您可以如下定义Y组合器:

One of the most epiphanous moments for me as a programmer was that the Y combinator is itself recursive. For example in Haskell you define Y combinator as follows:

y f = f (y f)

由于Haskell是一种惰性语言,因此仅在需要时才评估f (y f)中的y f,因此您不会陷入无限循环.在内部,Haskell为每个表达式创建一个thunk.但是,在JavaScript中,您需要显式创建一个thunk:

Because Haskell is a lazy language the y f in f (y f) is only evaluated when required and hence you don't run into an infinite loop. Internally Haskell creates a thunk for every expression. In JavaScript however you need to create a thunk explicitly:

function y(f) {
    return function () {
        return f(y(f)).apply(this, arguments);
    };
}

当然,以递归方式定义Y组合器是作弊的:您只是在Y组合器内部显式地递归.在数学上,应以非递归方式定义Y组合器,以描述递归的结构.尽管如此,我们都喜欢它.重要的是,JavaScript中的Y组合器现在返回一个thunk(即,我们使用惰性语义定义了它).

Of course defining the Y combinator recursively is cheating: you are just explicitly recursing inside the Y combinator instead. Mathematically the Y combinator itself should be defined non-recursively to describe the structure of recursion. Nonetheless we all love it anyway. The important thing is that the Y combinator in JavaScript now returns a thunk (i.e. we defined it using lazy semantics).

为巩固我们的理解,让我们在JavaScript中创建另一个惰性函数.让我们在JavaScript中实现Haskell的repeat函数.在Haskell中,repeat函数的定义如下:

To consolidate our understanding let's create another lazy function in JavaScript. Let's implement the repeat function from Haskell in JavaScript. In Haskell the repeat function is defined as follows:

repeat :: a -> [a]
repeat x = x : repeat x

如您所见,repeat没有边缘情况,并且它以递归方式调用自身.如果Haskell不那么懒惰,它将永远递归.如果JavaScript很懒,那么我们可以按以下方式实现repeat:

As you can see repeat has no edge cases and it calls itself recursively. If Haskell weren't so lazy it would recurse forever. If JavaScript were lazy then we could implement repeat as follows:

function repeat(x) {
    return [x, repeat(x)];
}

不幸的是,如果执行了上面的代码,它将永远递归直到导致堆栈溢出.为了解决这个问题,我们返回一个thunk:

Unfortunately if executed the above code would recurse forever until it results in a stack overflow. To solve this problem we return a thunk instead:

function repeat(x) {
    return function () {
        return [x, repeat(x)];
    };
}

当然,由于thunk不能评估函数,因此我们需要另一种方式来相同地对待thunk和正常值.因此,我们创建了一个函数来评估一个thunk,如下所示:

Of course since the thunk doesn't evaluate to a function we need another way to treat a thunk and a normal value identically. Hence we create a function to evaluate a thunk as follows:

function evaluate(thunk) {
    return typeof thunk === "function" ? thunk() : thunk;
}

evaluate函数现在可以用于实现可以将惰性数据结构或严格数据结构作为参数的函数.例如,我们可以使用evaluate从Haskell实现take函数.在Haskell中,take的定义如下:

The evaluate function can now be used to implement functions which can take either lazy or strict data structures as arguments. For example we can implement the take function from Haskell using evaluate. In Haskell take is defined as follows:

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

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

In JavaScript we would implement take using evaluate as follows:

function take(n, list) {
    if (n) {
        var xxs = evaluate(list);
        return xxs.length ? [xxs[0], take(n - 1, xxs[1])] : [];
    } else return [];
}

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

Now you can use repeat and take together as follows:

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.可以通过以下方式提升惰性函数来解决此问题:

In my humble opinion most thunks should be those returned by lazy functions. You should never have to create a thunk manually. However every time you create a lazy function you still need to create a thunk inside it manually. This problem can be solved by lifting lazy functions as follows:

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:

Using the lazy function you can now define the Y combinator and repeat as follows:

var y = lazy(function (f) {
    return f(y(f));
});

var repeat = lazy(function (x) {
    return [x, repeat(x)];
});

这使得JavaScript中的函数式编程几乎与Haskell或OCaml中的函数式编程一样有趣.请参阅更新的演示:

This makes functional programming in JavaScript almost as fun as functional programming in Haskell or OCaml. See the updated demo:

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;
        };
    };
}

有时,您需要将表达式传递给延迟求值的函数.在这种情况下,您需要创建自定义的thunk.因此,我们无法使用lazy函数.在这种情况下,您可以使用函数组合作为手动创建thunk的可行替代方法.函数组成在Haskell中定义如下:

Sometimes you need to pass expressions to functions that are evaluated lazily. In such situations you need to create custom thunks. Hence we can't make use of the lazy function. In such cases you can use function composition as a viable alternative to manually creating thunks. Function composition is defined as follows in Haskell:

(.) :: (b -> c) -> (a -> b) -> a -> c
f . g = \x -> f (g x)

在JavaScript中,这表示为:

In JavaScript this translates to:

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 composition in mathematics reads from right-to-left. However evaluation in JavaScript is always from left-to-right. For example in the expression slow_foo().toUpperCase() the function slow_foo is executed first and then the method toUpperCase is called on its return value. Hence we want to compose functions in reverse order and chain them as follows:

Function.prototype.pipe = function (f) {
    var g = this;

    return function () {
        return f(g.apply(this, arguments));
    };
};

使用pipe方法,我们现在可以组成以下功能:

Using the pipe method we can now compose functions as follows:

var toUpperCase = "".toUpperCase;
slow_foo.pipe(toUpperCase);

上面的代码将等效于以下重击:

The above code will be equivalent to the following thunk:

function () {
    return toUpperCase(slow_foo.apply(this, arguments));
}

但是有问题. toUpperCase函数实际上是一种方法.因此,由slow_foo返回的值应设置toUpperCasethis指针.简而言之,我们希望将slow_foo的输出通过管道传递到toUpperCase,如下所示:

However there's a problem. The toUpperCase function is actually a method. Hence the value returned by slow_foo should set the this pointer of toUpperCase. In short we want to pipe the output of slow_foo into toUpperCase as follows:

function () {
    return slow_foo.apply(this, arguments).toUpperCase();
}

该解决方案实际上非常简单,我们根本不需要修改pipe方法:

The solution is actually very simple and we don't need to modify our pipe method at all:

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方法,我们现在可以如下重构代码:

Using the callable method we can now refactor our code as follows:

var toUpperCase = "".toUpperCase;
slow_foo.pipe(callable(toUpperCase));

由于callable(toUpperCase)等同于toUpperCase.call,我们的重击现在为:

Since callable(toUpperCase) is equivalent to toUpperCase.call our thunk is now:

function () {
    return toUpperCase.call(slow_foo.apply(this, arguments));
}

这正是我们想要的.因此,我们的最终代码如下:

This is exactly what we want. Hence our final code is as follows:

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;
}

然后我们定义测试用例:

Then we define the test case:

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函数使举升函数返回浮点,也可以使用组合函数来创建新的浮点.

Hence as you can see for most cases you never need to create thunks manually. Either lift functions using the function lazy to make them return thunks or compose functions to create new thunks.

这篇关于比function(){return x}更简洁的延迟评估?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆