私の謙虚な意見では、あなたはこの問題を間違った視点から見ていると思います. サンクを手動で作成している場合は、コードのリファクタリングを検討する必要があります。ほとんどの場合、サンクは次のようになります。
- どちらかが遅延関数から返されました。
- または、関数を合成して作成します。
遅延関数からサンクを返す
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 f
inf (y f)
は必要な場合にのみ評価されるため、無限ループに陥ることはありません。内部的に、Haskell はすべての式に対してサンクを作成します。ただし JavaScript では、サンクを明示的に作成する必要があります。
function y(f) {
return function () {
return f(y(f)).apply(this, arguments);
};
}
もちろん、Y コンビネータを再帰的に定義するのはごまかしです。代わりに、Y コンビネータ内で明示的に再帰しているだけです。数学的には、再帰の構造を記述するために、Y コンビネータ自体を非再帰的に定義する必要があります。それにもかかわらず、私たちはとにかくそれを愛しています。重要なことは、JavaScript の Y コンビネータがサンクを返すようになったことです (つまり、遅延セマンティクスを使用して定義しました)。
理解を深めるために、JavaScript で別の遅延関数を作成してみましょう。repeat
Haskell の関数を 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
設定する必要があります。要するに、出力を次のようにパイプします。this
toUpperCase
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
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
をリフトしてサンクを返すようにするか、関数を構成して新しいサンクを作成します。