U tổ hợp
Bằng cách truyền một hàm cho chính nó dưới dạng đối số, một hàm có thể lặp lại bằng cách sử dụng tham số thay vì tên của nó! Vì vậy, hàm được cho U
phải có ít nhất một tham số sẽ liên kết với hàm (chính nó).
Trong ví dụ dưới đây, chúng tôi không có điều kiện thoát, vì vậy chúng tôi sẽ chỉ lặp vô thời hạn cho đến khi xảy ra tràn ngăn xếp
const U = f => f (f) // call function f with itself as an argument
U (f => (console.log ('stack overflow imminent!'), U (f)))
Chúng ta có thể dừng đệ quy vô hạn bằng nhiều kỹ thuật khác nhau. Ở đây, tôi sẽ viết hàm ẩn danh của chúng tôi để trả về một hàm ẩn danh khác đang chờ đầu vào; trong trường hợp này, một số. Khi một số được cung cấp, nếu nó lớn hơn 0, chúng tôi sẽ tiếp tục lặp lại, nếu không thì trả về 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
Điều không rõ ràng ngay lập tức ở đây là hàm của chúng ta, khi lần đầu tiên được áp dụng cho chính nó bằng cách sử dụng bộ U
tổ hợp, nó trả về một hàm đang chờ đầu vào đầu tiên. Nếu chúng ta đã đặt tên cho nó, có thể tạo một cách hiệu quả các hàm đệ quy bằng lambdas (hàm ẩn danh)
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
Chỉ có điều này không phải là đệ quy trực tiếp - một hàm tự gọi nó bằng tên riêng của nó. Định nghĩa của chúng tôi về countDown
không tự tham chiếu bên trong cơ thể của nó và vẫn có thể đệ quy
// 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)
Cách xóa tự tham chiếu khỏi một hàm hiện có bằng bộ tổ hợp U
Ở đây tôi sẽ chỉ cho bạn cách sử dụng một hàm đệ quy sử dụng một tham chiếu đến chính nó và thay đổi nó thành một hàm sử dụng bộ tổ hợp U thay cho tham chiếu tự
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
Bây giờ sử dụng bộ tổ hợp U để thay thế tham chiếu bên trong factorial
const U = f => f (f)
const factorial = U (f => x =>
x === 0 ? 1 : x * U (f) (x - 1))
console.log (factorial (5)) // 120
Mô hình thay thế cơ bản là thế này. Hãy ghi nhớ, chúng ta sẽ sử dụng một chiến lược tương tự trong phần tiếp theo
// self reference recursion
const foo = x => ... foo (nextX) ...
// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)
Bộ tổ hợp Y
liên quan: các tổ hợp U và Y được giải thích bằng cách sử dụng phép tương tự trong gương
Trong phần trước, chúng ta đã biết cách chuyển đổi đệ quy tự tham chiếu thành một hàm đệ quy không dựa vào một hàm đã đặt tên bằng cách sử dụng bộ tổ hợp U. Có một chút khó chịu khi phải nhớ luôn chuyển hàm cho chính nó làm đối số đầu tiên. Chà, bộ tổ hợp Y được xây dựng dựa trên bộ tổ hợp chữ U và loại bỏ phần tẻ nhạt đó. Đây là một điều tốt vì loại bỏ / giảm độ phức tạp là lý do chính mà chúng tôi tạo ra các hàm
Đầu tiên, hãy lấy bộ tổ hợp Y của riêng chúng ta
// 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)))
Bây giờ chúng ta sẽ xem cách sử dụng của nó so với bộ tổ hợp chữ U. Lưu ý, để lặp lại, thay vì U (f)
chúng ta có thể chỉ cần gọif ()
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
Y (f => (console.log ('stack overflow imminent!'), f ()))
Bây giờ tôi sẽ trình bày countDown
chương trình bằng cách sử dụng Y
- bạn sẽ thấy các chương trình gần như giống hệt nhau nhưng bộ tổ hợp Y giúp mọi thứ sạch sẽ hơn một chút
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
Và bây giờ chúng ta sẽ thấy factorial
cũng
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
Như bạn có thể thấy, f
trở thành cơ chế cho chính nó đệ quy. Để lặp lại, chúng tôi gọi nó giống như một hàm thông thường. Chúng ta có thể gọi nó nhiều lần với các đối số khác nhau và kết quả sẽ vẫn đúng. Và vì nó là một tham số hàm thông thường, chúng ta có thể đặt tên nó bất cứ thứ gì chúng ta muốn, chẳng hạn như recur
bên dưới:
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
Bộ tổ hợp U và Y với nhiều hơn 1 tham số
Trong các ví dụ trên, chúng ta đã thấy cách chúng ta có thể lặp lại và truyền một đối số để theo dõi "trạng thái" tính toán của chúng ta. Nhưng nếu chúng ta cần theo dõi trạng thái bổ sung thì sao?
Chúng ta có thể sử dụng dữ liệu phức hợp như Mảng hoặc thứ gì đó ...
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
Nhưng điều này thật tệ vì nó để lộ trạng thái bên trong (bộ đếm a
và b
). Thật tuyệt nếu chúng ta có thể gọi điện fibonacci (7)
để nhận được câu trả lời mình muốn.
Sử dụng những gì chúng ta biết về các hàm đơn phân (chuỗi các hàm một bậc (1 tham số)), chúng ta có thể đạt được mục tiêu của mình một cách dễ dàng mà không cần phải sửa đổi định nghĩa của chúng ta về Y
hoặc dựa vào dữ liệu phức hợp hoặc các tính năng ngôn ngữ nâng cao.
Nhìn kỹ định nghĩa của fibonacci
bên dưới. Chúng tôi ngay lập tức áp dụng 0
và 1
được ràng buộc a
và b
tương ứng. Bây giờ fibonacci chỉ đơn giản là đợi đối số cuối cùng được cung cấp mà sẽ bị ràng buộc x
. Khi chúng ta đệ quy, chúng ta phải gọi f (a) (b) (x)
(not f (a,b,x)
) vì hàm của chúng ta ở dạng curry.
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
Loại mẫu này có thể hữu ích để xác định tất cả các loại chức năng. Dưới đây chúng ta sẽ thấy thêm hai chức năng được xác định bằng cách sử dụng Y
combinator ( range
và reduce
) và một dẫn xuất của 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 ]
ĐÓ LÀ TẤT CẢ BẤT CỨ KHỔNG LỒ
Bởi vì chúng tôi đang làm việc với các hàm thuần túy ở đây, chúng tôi có thể thay thế bất kỳ hàm nào được đặt tên cho định nghĩa của nó. Xem điều gì sẽ xảy ra khi chúng ta lấy fibonacci và thay thế các hàm được đặt tên bằng các biểu thức của chúng
/* 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
Và bạn đã có nó - được fibonacci (7)
tính toán đệ quy không sử dụng gì ngoài các hàm ẩn danh