U kombinator
Dengan meneruskan fungsi ke dirinya sendiri sebagai argumen, sebuah fungsi bisa berulang menggunakan parameternya alih-alih namanya! Jadi fungsi yang diberikan U
harus memiliki setidaknya satu parameter yang akan mengikat fungsi (itu sendiri).
Dalam contoh di bawah ini, kami tidak memiliki kondisi keluar, jadi kami hanya akan mengulang tanpa batas hingga tumpukan overflow terjadi
const U = f => f (f) // call function f with itself as an argument
U (f => (console.log ('stack overflow imminent!'), U (f)))
Kami dapat menghentikan rekursi tak terbatas menggunakan berbagai teknik. Di sini, saya akan menulis fungsi anonim kami untuk mengembalikan fungsi anonim lain yang menunggu masukan; dalam hal ini, beberapa nomor. Ketika angka diberikan, jika lebih besar dari 0, kami akan terus berulang, jika tidak mengembalikan 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
Apa yang tidak langsung terlihat di sini adalah bahwa fungsi kita, saat pertama kali diterapkan ke dirinya sendiri menggunakan U
kombinator, ia mengembalikan fungsi yang menunggu masukan pertama. Jika kami memberi nama untuk ini, dapat secara efektif membangun fungsi rekursif menggunakan lambda (fungsi anonim)
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
Hanya ini bukan rekursi langsung - fungsi yang memanggil dirinya sendiri menggunakan namanya sendiri. Definisi kami tentang countDown
tidak merujuk dirinya sendiri di dalam tubuhnya dan rekursi tetap dimungkinkan
// 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)
Cara menghapus referensi mandiri dari fungsi yang ada menggunakan kombinator U.
Di sini saya akan menunjukkan kepada Anda cara mengambil fungsi rekursif yang menggunakan referensi ke dirinya sendiri dan mengubahnya menjadi fungsi yang menggunakan kombinator U sebagai pengganti referensi mandiri.
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
Sekarang menggunakan kombinator U untuk mengganti referensi bagian dalam factorial
const U = f => f (f)
const factorial = U (f => x =>
x === 0 ? 1 : x * U (f) (x - 1))
console.log (factorial (5)) // 120
Pola penggantian dasarnya adalah ini. Buat catatan mental, kami akan menggunakan strategi serupa di bagian selanjutnya
// self reference recursion
const foo = x => ... foo (nextX) ...
// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)
Kombinator Y
terkait: kombinator U dan Y dijelaskan menggunakan analogi cermin
Pada bagian sebelumnya kita telah melihat bagaimana mengubah rekursi referensi diri menjadi fungsi rekursif yang tidak bergantung pada fungsi bernama menggunakan kombinator U. Ada sedikit gangguan karena harus ingat untuk selalu meneruskan fungsi ke dirinya sendiri sebagai argumen pertama. Nah, kombinator-Y dibangun di atas kombinator-U dan menghilangkan bagian yang membosankan itu. Ini adalah hal yang baik karena menghilangkan / mengurangi kompleksitas adalah alasan utama kami membuat fungsi
Pertama, mari turunkan kombinator-Y kita sendiri
// 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)))
Sekarang kita akan melihat bagaimana penggunaannya dibandingkan dengan U-combinator. Perhatikan, untuk berulang, alih-alih U (f)
kita bisa langsung meneleponf ()
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
Y (f => (console.log ('stack overflow imminent!'), f ()))
Sekarang saya akan mendemonstrasikan penggunaan countDown
program Y
- Anda akan melihat programnya hampir identik tetapi kombinator Y menjaga semuanya sedikit lebih bersih
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
Dan sekarang kita akan lihat factorial
juga
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
Seperti yang Anda lihat, f
menjadi mekanisme rekursi itu sendiri. Untuk berulang, kami menyebutnya seperti fungsi biasa. Kita bisa memanggilnya berkali-kali dengan argumen berbeda dan hasilnya akan tetap benar. Dan karena ini adalah parameter fungsi biasa, kita dapat menamainya sesuka kita, seperti di recur
bawah ini -
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
Kombinator U dan Y dengan lebih dari 1 parameter
Pada contoh di atas, kita melihat bagaimana kita dapat mengulang dan meneruskan argumen untuk melacak "status" dari komputasi kita. Tetapi bagaimana jika kita perlu melacak status tambahan?
Kita bisa menggunakan data gabungan seperti Array atau sesuatu ...
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
Tapi ini buruk karena mengekspos keadaan internal (penghitung a
dan b
). Alangkah baiknya jika kita bisa menelepon fibonacci (7)
untuk mendapatkan jawaban yang kita inginkan.
Menggunakan apa yang kita ketahui tentang fungsi curried (urutan fungsi unary (1-paramter)), kita dapat mencapai tujuan kita dengan mudah tanpa harus mengubah definisi kita Y
atau mengandalkan data gabungan atau fitur bahasa tingkat lanjut.
Perhatikan definisi di fibonacci
bawah ini. Kami segera melamar 0
dan 1
yang terikat a
dan b
masing - masing. Sekarang fibonacci hanya menunggu argumen terakhir yang akan diberikan x
. Ketika kita berulang, kita harus memanggil f (a) (b) (x)
(bukan f (a,b,x)
) karena fungsi kita dalam bentuk kari.
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
Pola semacam ini dapat berguna untuk mendefinisikan semua jenis fungsi. Di bawah ini kita akan melihat dua fungsi yang lebih didefinisikan dengan menggunakan Y
combinator ( range
dan reduce
) dan turunan dari 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 ]
INI SEMUA ANONIM OMG
Karena kami bekerja dengan fungsi murni di sini, kami dapat mengganti fungsi bernama apa pun untuk definisinya. Perhatikan apa yang terjadi saat kita menggunakan fibonacci dan mengganti fungsi bernama dengan ekspresinya
/* 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
Dan begitulah - fibonacci (7)
dihitung secara rekursif hanya dengan menggunakan fungsi anonim