3

遅延評価に大きく依存する Python コードをいくつか移植しています。これはthunksによって実現されます。より具体的には、<expr>遅延評価が必要な Python 式は、Python の「ラムダ式」、つまりlambda:<expr>.

私の知る限り、これに最も近い JavaScript はfunction(){return <expr>}.

私が取り組んでいるコードはそのようなサンクで完全にあふれているため、可能であれば、それらのコードをより簡潔にしたいと考えています。この理由は、文字を節約するため (JS に関しては無視できない考慮事項) だけでなく、コードをより読みやすくするためでもあります。私が言いたいことを理解するには、次の標準の JavaScript フォームを比較してください。

function(){return fetchx()}

\fetchx()

最初の形式では、実質的な情報、つまり式は、周囲の...fetchx()によって誤植的に隠されています。2 番目の形式1では、 「遅延評価マーカー」として( ) 文字が 1 つだけ使用されます。これが最適なアプローチだと思います2function(){return}\

AFAICT によると、この問題の解決策は次のカテゴリに分類されます。

  1. eval遅延評価をシミュレートするために使用します。
  2. 私が知らない特別な JavaScript 構文がいくつかありますが、それは私が望むことを実現します。(私は JavaScript について非常に無知なので、この可能性は非常に現実的に見えます。)
  3. プログラムによって正しい JavaScript に処理される非標準の JavaScript でコードを記述します。(もちろん、このアプローチは最終的なコードのフットプリントを減らすことはありませんが、少なくとも読みやすさの向上を維持する可能性があります。)
  4. 上記のどれでもない。

最後の 3 つのカテゴリの回答を聞くことに特に興味があります。


PS: 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どちらの関数も、 object 、 key key、および default 値の3 つの引数を取り、 が に存在する場合は両方とも、存在しないobj[key]場合はデフォルト値を返します。keyobj

2 つの関数の唯一の違いは、 のデフォルト値がlazygetthunk になる可能性があることです。その場合、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);
}

これら 2 つの関数の動作を確認するには、次のように定義します。

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 などで使用されています。このアプローチでは、関数の定義の一部として、非標準評価用の仮引数のいずれかを指定できます。タイポグラフィ的には、このアプローチは確かに最大限控えめですが、私の好みには少し多すぎます。\その上、それは、たとえば遅延評価マーカー として使用するほど柔軟ではありません。

4

2 に答える 2

5

私の謙虚な意見では、あなたはこの問題を間違った視点から見ていると思います. サンクを手動で作成している場合は、コードのリファクタリングを検討する必要があります。ほとんどの場合、サンクは次のようになります。

  1. どちらかが遅延関数から返されました。
  2. または、関数を合成して作成します。

遅延関数からサンクを返す

JavaScript で関数型プログラミングの練習を始めたとき、私はY コンビネータに戸惑いました。私がオンラインで読んだことによると、Y コンビネーターは崇拝すべき神聖な存在でした。どういうわけか、自分自身の名前を知らない関数が自分自身を呼び出すことができました。したがって、これは再帰の数学的表現であり、関数型プログラミングの最も重要な柱の 1 つです。

しかし、Y コンビネータを理解するのは簡単なことではありませんでした。Mike Vanier、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 コンビネータが再帰の表現であることがわかりません。x(x)言うまでもなく、JavaScript のような熱心な言語では、式がすぐに評価されて無限ループが発生し、最終的にスタック オーバーフローが発生するため、まったく使用できません。したがって、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); }です。サンクに包まれています。ただし、JavaScript では、サンクを次のように記述する方が理にかなっています。

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

もちろん、ここでは がx(x)関数に評価されると仮定しています。Y コンビネータの場合、これは実際に当てはまります。ただし、サンクが関数に評価されない場合は、単に式を返します。


プログラマーとしての私にとって最もひらめいた瞬間の 1 つは、Y コンビネーター自体が再帰的であるということでした。たとえば Haskell では、次のように Y コンビネータを定義します。

y f = f (y f)

Haskell は遅延言語であるため、y finf (y f)は必要な場合にのみ評価されるため、無限ループに陥ることはありません。内部的に、Haskell はすべての式に対してサンクを作成します。ただし JavaScript では、サンクを明示的に作成する必要があります。

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

もちろん、Y コンビネータを再帰的に定義するのはごまかしです。代わりに、Y コンビネータ内で明示的に再帰しているだけです。数学的には、再帰の構造を記述するために、Y コンビネータ自体を非再帰的に定義する必要があります。それにもかかわらず、私たちはとにかくそれを愛しています。重要なことは、JavaScript の Y コンビネータがサンクを返すようになったことです (つまり、遅延セマンティクスを使用して定義しました)。


理解を深めるために、JavaScript で別の遅延関数を作成してみましょう。repeatHaskell の関数を JavaScript で実装してみましょう。Haskell では、repeat関数は次のように定義されます。

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

ご覧のとおりrepeat、エッジ ケースはなく、自身を再帰的に呼び出します。Haskell がそれほど怠け者でなければ、永遠に再帰するでしょう。JavaScript が遅延型である場合repeat、次のように実装できます。

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

残念ながら、上記のコードを実行すると、スタック オーバーフローが発生するまで永久に再帰します。この問題を解決するために、代わりにサンクを返します。

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

もちろん、サンクは関数に評価されないため、サンクと通常の値を同じように扱う別の方法が必要です。したがって、次のようにサンクを評価する関数を作成します。

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

このevaluate関数を使用して、遅延データ構造または厳密データ構造を引数として取ることができる関数を実装できるようになりました。たとえば、 をtake使用して Haskell の関数を実装できますevaluate。Haskelltakeでは、次のように定義されています。

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

repeatこれで、次のように と をtake一緒に使用できます。

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

職場での遅延評価。


私の謙虚な意見では、ほとんどのサンクは遅延関数によって返されるものであるべきです。サンクを手動で作成する必要はありません。ただし、遅延関数を作成するたびに、その中にサンクを手動で作成する必要があります。この問題は、次のように遅延関数を持ち上げることで解決できます。

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

関数を使用して、Y コンビネータを次のようlazyに定義できるようになりました。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;
        };
    };
}

関数の合成によるサンクの作成

遅延評価される関数に式を渡す必要がある場合があります。このような状況では、カスタム サンクを作成する必要があります。したがって、関数を使用することはできませんlazy。このような場合、手動でサンクを作成する代わりに、関数合成を使用できます。関数合成は、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);

上記のコードは、次のサンクと同等になります。

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

しかし、問題があります。toUpperCase関数は実際にはメソッドです。したがって、によって返される値は、のポインターをslow_foo設定する必要があります。要するに、出力を次のようにパイプします。thistoUpperCaseslow_footoUpperCase

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

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

したがって、ほとんどの場合でわかるように、サンクを手動で作成する必要はありません。関数を使用して関数lazyをリフトしてサンクを返すようにするか、関数を構成して新しいサンクを作成します。

于 2013-11-08T15:34:04.823 に答える