Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
272 views
in Technique[技术] by (71.8m points)

javascript - More succinct delayed evaluation than function(){return x}?

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, the closest JavaScript equivalent of this is function(){return <expr>}.

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()}

with

fetchx()

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, solutions to this problem would fall into the following categories:

  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.: 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.

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()}

to

C('fetchx()')

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

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

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.

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

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

prints out

0
quick!
1
quick!
1
SORRY FOR THE WAIT!
2
slow_foo().toUpperCase()
2

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

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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. Either returned from lazy functions.
  2. Or created by composing functions.

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.

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

In JavaScript this would translate to:

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

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

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

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

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.


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)

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

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).


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

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

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

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

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

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

Now you can use repeat and take together as follows:

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

See the demo for yourself:

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...