U-комбинатор
U-комбинатор берет функцию и применяет ее к себе.Таким образом, функция, которую вы ей даете, должна иметь по крайней мере один параметр, который будет привязан к самой функции
В приведенном ниже примере у нас нет условия выхода, поэтому мы будем просто бесконечно зацикливаться, пока не произойдет переполнение стека
const U = f => f (f)
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 <b>loop</b> (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 (<b>x =></b> Y (f) <b>(x)</b>)
// remove reference to self using U combinator
const Y = <b>U (h =></b> f => f (x => <b>U (h)</b> (f) (x))<b>)</b>
Теперь посмотрим, как его использование сравнивается.к 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
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 ]
ЭТО ВСЕ АНОНИМНОЕ 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 => U (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)
вычисляется рекурсивно, используя только анонимные функции