U combinator
การส่งผ่านฟังก์ชันให้ตัวเองเป็นอาร์กิวเมนต์ฟังก์ชันสามารถเกิดซ้ำได้โดยใช้พารามิเตอร์แทนชื่อของมัน! ดังนั้นฟังก์ชันที่กำหนดให้U
ควรมีอย่างน้อยหนึ่งพารามิเตอร์ที่จะผูกกับฟังก์ชัน (ตัวเอง)
ในตัวอย่างด้านล่างเราไม่มีเงื่อนไขการออกดังนั้นเราจะวนซ้ำไปเรื่อย ๆ จนกว่าสแต็กล้นจะเกิดขึ้น
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
รวมกันจะส่งกลับฟังก์ชันที่รออินพุตแรก หากเราตั้งชื่อให้สิ่งนี้สามารถสร้างฟังก์ชันเรียกซ้ำได้อย่างมีประสิทธิภาพโดยใช้ lambdas (ฟังก์ชันที่ไม่ระบุชื่อ)
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 combinator
ที่นี่ฉันจะแสดงวิธีใช้ฟังก์ชันเรียกซ้ำที่ใช้การอ้างอิงถึงตัวมันเองและเปลี่ยนเป็นฟังก์ชันที่ใช้ U combinator แทนการอ้างอิงตัวเอง
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
ตอนนี้ใช้ U combinator เพื่อแทนที่การอ้างอิงภายใน 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 combinator
ที่เกี่ยวข้อง: ตัวรวม U และ Y อธิบายโดยใช้การเปรียบเทียบกระจก
ในส่วนก่อนหน้านี้เราได้เห็นวิธีการเปลี่ยนการเรียกซ้ำการอ้างอิงตัวเองเป็นฟังก์ชันเรียกซ้ำที่ไม่ต้องอาศัยฟังก์ชันที่ตั้งชื่อโดยใช้ U combinator มีความน่ารำคาญเล็กน้อยที่ต้องจำไว้ว่าให้ส่งฟังก์ชันให้ตัวเองเป็นอาร์กิวเมนต์แรกเสมอ Y-combinator สร้างขึ้นจาก U-combinator และกำจัดบิตที่น่าเบื่อนั้นออกไป นี่เป็นสิ่งที่ดีเพราะการลบ / ลดความซับซ้อนเป็นเหตุผลหลักที่เราสร้างฟังก์ชัน
ก่อนอื่นมาหา Y-combinator ของเราเอง
// 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 combinator ช่วยให้ทุกอย่างสะอาดขึ้น
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 Combinator ที่มีพารามิเตอร์มากกว่า 1 ตัว
ในตัวอย่างข้างต้นเราได้เห็นวิธีที่เราสามารถวนซ้ำและส่งผ่านอาร์กิวเมนต์เพื่อติดตาม "สถานะ" ของการคำนวณของเรา แต่ถ้าเราต้องติดตามสถานะเพิ่มเติมล่ะ?
เราสามารถใช้ข้อมูลประกอบเช่น Array หรือบางสิ่ง ...
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)
เพื่อรับคำตอบที่เราต้องการได้
การใช้สิ่งที่เรารู้เกี่ยวกับฟังก์ชัน curried (ลำดับของฟังก์ชัน unary (1-paramter) เราสามารถบรรลุเป้าหมายได้อย่างง่ายดายโดยไม่ต้องปรับเปลี่ยนคำจำกัดความของเราY
หรือพึ่งพาข้อมูลผสมหรือคุณลักษณะของภาษาขั้นสูง
ดูคำจำกัดความของfibonacci
ด้านล่างอย่างใกล้ชิด เรากำลังสมัครทันที0
และ1
ที่ผูกพันa
และb
ตามลำดับ ตอนนี้ fibonacci x
เป็นเพียงการรอคอยสำหรับอาร์กิวเมนต์สุดท้ายที่จะจัดจำหน่ายซึ่งจะถูกผูกไว้กับ เมื่อเราเรียกคืนเราต้องเรียกf (a) (b) (x)
(ไม่ใช่f (a,b,x)
) เพราะฟังก์ชันของเราอยู่ในรูปแบบ curried
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
รูปแบบการจัดเรียงนี้มีประโยชน์สำหรับการกำหนดฟังก์ชันทุกประเภท ด้านล่างเราจะเห็นทั้งสองฟังก์ชั่นอื่น ๆ ที่กำหนดไว้โดยใช้Y
Combinator ( 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 ที่ไม่ระบุชื่อทั้งหมด
เนื่องจากเรากำลังทำงานกับฟังก์ชันบริสุทธิ์ที่นี่เราจึงสามารถแทนที่ฟังก์ชันที่มีชื่อสำหรับคำจำกัดความได้ ดูสิ่งที่เกิดขึ้นเมื่อเราใช้ fibonacci และแทนที่ฟังก์ชันที่มีชื่อด้วยนิพจน์
/* 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)
คำนวณซ้ำโดยไม่ใช้อะไรเลยนอกจากฟังก์ชันที่ไม่ระบุ