U комбінатор
Передаючи функцію собі як аргумент, функція може повторюватися, використовуючи свій параметр замість свого імені! Отже, функція, надана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
комбінатора повертає функцію, що чекає першого вводу. Якщо ми дамо ім'я цьому, можемо ефективно конструювати рекурсивні функції за допомогою лямбда (анонімні функції)
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-комбінатором. Зауважте, щоб повторити, замість цього 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 комбінатор з більш ніж 1 параметром
У наведених вище прикладах ми бачили, як ми можемо циклічно передавати аргумент та відслідковувати "стан" нашого обчислення. Але що робити, якщо нам потрібно вести облік додаткового стану?
Ми можемо використовувати складні дані, такі як масив чи щось таке ...
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
Такий зразок може бути корисним для визначення всіляких функцій. Нижче ми побачимо ще дві функції , визначені з допомогою 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 ]
ВСІ АНОНИМНІ ОМГ
Оскільки тут ми працюємо з чистими функціями, ми можемо замінити будь-яку названу функцію для її визначення. Подивіться, що відбувається, коли ми візьмемо поле і замінимо названі функції їх виразами
/* 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)
обчислено рекурсивно, не використовуючи нічого, крім анонімних функцій