Uコンビネーター
関数を引数としてそれ自体に渡すことにより、関数は名前の代わりにそのパラメーターを使用して再帰できます!したがって、指定さU
れた関数には、関数(それ自体)にバインドされる少なくとも1つのパラメーターが必要です。
以下の例では、終了条件がないため、スタックオーバーフローが発生するまで無限にループします
const U = f => f (f) // call function f with itself as an argument
U (f => (console.log ('stack overflow imminent!'), U (f)))
さまざまな手法を使用して、無限再帰を停止できます。ここでは、入力を待機している別の匿名関数を返す匿名関数を記述します。この場合、いくつかの数。数値が指定された場合、0より大きい場合は繰り返しが続行され、それ以外の場合は0が返されます。
const log = x => (console.log (x), x)
const U = f => f (f)
// when our function is applied to itself, we get the inner function back
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
// returns: (x => x > 0 ? U (f) (log (x - 1)) : 0)
// where f is a reference to our outer function
// watch when we apply an argument to this function, eg 5
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0) (5)
// 4 3 2 1 0
ここですぐに明らかにならないのは、関数がU
コンビネーターを使用して最初に適用されると、最初の入力を待つ関数を返すことです。これに名前を付けると、ラムダ(無名関数)を使用して再帰関数を効率的に構築できます
const log = x => (console.log (x), x)
const U = f => f (f)
const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
countDown (5)
// 4 3 2 1 0
countDown (3)
// 2 1 0
これだけが直接再帰ではありません。独自の名前を使用して自分自身を呼び出す関数です。私たちの定義はcountDown
自分の体の中でそれ自体を参照しておらず、それでも再帰が可能です
// direct recursion references itself by name
const loop = (params) => {
if (condition)
return someValue
else
// loop references itself to recur...
return loop (adjustedParams)
}
// U combinator does not need a named reference
// no reference to `countDown` inside countDown's definition
const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
Uコンビネーターを使用して既存の関数から自己参照を削除する方法
ここでは、それ自体への参照を使用する再帰関数を取得し、それをU結合子を使用する関数に変更して、自己参照の代わりにする方法を示します。
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
Uコンビネータを使用して、内部参照を factorial
const U = f => f (f)
const factorial = U (f => x =>
x === 0 ? 1 : x * U (f) (x - 1))
console.log (factorial (5)) // 120
基本的な交換パターンはこちらです。メンタルなメモを作ってください。次のセクションでは同様の戦略を使用します
// self reference recursion
const foo = x => ... foo (nextX) ...
// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)
Yコンビネーター
関連:ミラーアナロジーを使用して説明されたUおよびYコンビネーター
前のセクションでは、Uコンビネーターを使用して、名前付き関数に依存しない自己参照再帰を再帰関数に変換する方法を説明しました。最初の引数として常に自分自身に関数を渡すことを覚えておかなければならないことで、少し面倒があります。さて、YコンビネーターはUコンビネーターに基づいて構築され、その面倒なビットを削除します。複雑さを取り除く/減らすことが私たちが関数を作る主な理由なので、これは良いことです
最初に、独自のYコンビネータを導出しましょう
// standard definition
const Y = f => f (Y (f))
// prevent immediate infinite recursion in applicative order language (JS)
const Y = f => f (x => Y (f) (x))
// remove reference to self using U combinator
const Y = U (h => f => f (x => U (h) (f) (x)))
次に、U-combinatorと比較して使用方法を確認します。繰り返しますが、U (f)
単に呼び出す代わりにf ()
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
Y (f => (console.log ('stack overflow imminent!'), f ()))
ここで、countDown
プログラムを使用してデモンストレーションしY
ます。プログラムはほとんど同じですが、Yコンビネータにより、少しクリーンになっています。
const log = x => (console.log (x), x)
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const countDown = Y (f => x => x > 0 ? f (log (x - 1)) : 0)
countDown (5)
// 4 3 2 1 0
countDown (3)
// 2 1 0
そして今、我々は表示されますfactorial
だけでなく
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const factorial = Y (f => x =>
x === 0 ? 1 : x * f (x - 1))
console.log (factorial (5)) // 120
ご覧のとおりf
、再帰自体のメカニズムになります。繰り返しますが、通常の関数のように呼び出します。異なる引数を使用して複数回呼び出すことができ、結果は正しいままです。そしてそれは通常の関数パラメーターなので、recur
以下のように好きな名前を付けることができます-
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (recur => n =>
n < 2 ? n : recur (n - 1) + (n - 2))
console.log (fibonacci (10)) // 55
複数のパラメーターを持つUおよびYコンビネーター
上記の例では、計算の「状態」を追跡するためにループして引数を渡す方法を示しました。しかし、追加の状態を追跡する必要がある場合はどうでしょうか。
我々は可能性が配列か何かのような化合物のデータを使用して...
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (f => ([a, b, x]) =>
x === 0 ? a : f ([b, a + b, x - 1]))
// starting with 0 and 1, generate the 7th number in the sequence
console.log (fibonacci ([0, 1, 7]))
// 0 1 1 2 3 5 8 13
しかし、これは内部状態(カウンターa
とb
)を公開しているので悪いです。必要なfibonacci (7)
答えを得るために電話をかけるだけでいいのですが。
カリー関数(単項(1パラメーター)関数のシーケンス)について知っていることを使用してY
、複合データの定義を変更したり、複合データや高度な言語機能に依存したりすることなく、目的を簡単に達成できます。
fibonacci
以下の定義をよく見てください。私たちは、すぐに適用している0
と1
に結合しているa
とb
、それぞれ。現在、フィボナッチは、バインドされる最後の引数が提供されるのを単に待っていますx
。再帰するときは、関数がカリー化されているため、f (a) (b) (x)
(ではなくf (a,b,x)
)を呼び出す必要があります。
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (f => a => b => x =>
x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)
console.log (fibonacci (7))
// 0 1 1 2 3 5 8 13
この種のパターンは、あらゆる種類の関数を定義するのに役立ちます。以下は、我々は2つの関数が使用して定義されて表示されますY
コンビネータを(range
とreduce
)およびその誘導体reduce
、map
。
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const range = Y (f => acc => min => max =>
min > max ? acc : f ([...acc, min]) (min + 1) (max)) ([])
const reduce = Y (f => g => y => ([x,...xs]) =>
x === undefined ? y : f (g) (g (y) (x)) (xs))
const map = f =>
reduce (ys => x => [...ys, f (x)]) ([])
const add = x => y => x + y
const sq = x => x * x
console.log (range (-2) (2))
// [ -2, -1, 0, 1, 2 ]
console.log (reduce (add) (0) ([1,2,3,4]))
// 10
console.log (map (sq) ([1,2,3,4]))
// [ 1, 4, 9, 16 ]
それはすべて匿名のOMG
ここでは純粋な関数を使用しているので、名前付き関数をその定義の代わりに使用できます。フィボナッチを取り、名前付き関数をその式で置き換えるとどうなるかを見てください
/* const U = f => f (f)
*
* const Y = U (h => f => f (x => U (h) (f) (x)))
*
* const fibonacci = Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)
*
*/
/*
* given fibonacci (7)
*
* replace fibonacci with its definition
* Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
*
* replace Y with its definition
* U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
//
* replace U with its definition
* (f => f (f)) U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
*/
let result =
(f => f (f)) (h => f => f (x => h (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
console.log (result) // 13
そして、あなたはそれを持っています– fibonacci (7)
匿名関数だけを使って再帰的に計算されます