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)
чтобы получить нужный нам ответ.
Используя то, что мы знаем о каррированных функциях (последовательностях унарных (с одним параметром) функций), мы можем легко достичь нашей цели, не изменяя наше определение 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 ]
ЭТО ВСЕ АНОНИМНОЕ 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)
вычислено рекурсивно с использованием только анонимных функций.