Combinador U
Al pasar una función a sí misma como argumento, una función puede repetirse usando su parámetro en lugar de su nombre. Entonces la función dada aU
debe tener al menos un parámetro que se unirá a la función (en sí).
En el siguiente ejemplo, no tenemos una condición de salida, por lo que solo realizaremos un bucle indefinido hasta que ocurra un desbordamiento de pila.
const U = f => f (f) // call function f with itself as an argument
U (f => (console.log ('stack overflow imminent!'), U (f)))
Podemos detener la recursividad infinita usando una variedad de técnicas. Aquí, escribiré nuestra función anónima para devolver otra función anónima que está esperando una entrada; en este caso, algún número. Cuando se proporciona un número, si es mayor que 0, continuaremos recurriendo, de lo contrario devolveremos 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
Lo que no es evidente de inmediato aquí es que nuestra función, cuando se aplica por primera vez a sí misma utilizando el U
combinador, devuelve una función que espera la primera entrada. Si le damos un nombre a esto, podemos construir de manera efectiva funciones recursivas usando lambdas (funciones anónimas)
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
Solo que esto no es recursividad directa , una función que se llama a sí misma usando su propio nombre. Nuestra definición de countDown
no hace referencia a sí misma dentro de su cuerpo y aún así la recursividad es posible
// 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ómo eliminar la autorreferencia de una función existente usando el combinador U
Aquí le mostraré cómo tomar una función recursiva que usa una referencia a sí misma y cambiarla a una función que emplea el combinador U en lugar de la autorreferencia.
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
Ahora usando el combinador U para reemplazar la referencia interna a factorial
const U = f => f (f)
const factorial = U (f => x =>
x === 0 ? 1 : x * U (f) (x - 1))
console.log (factorial (5)) // 120
El patrón de reemplazo básico es este. Tome nota mentalmente, usaremos una estrategia similar en la siguiente sección
// self reference recursion
const foo = x => ... foo (nextX) ...
// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)
Combinador Y
relacionado: los combinadores U e Y explicados usando una analogía de espejo
En la sección anterior vimos cómo transformar la recursividad de autorreferencia en una función recursiva que no depende de una función nombrada usando el combinador U. Hay un poco de molestia aunque tener que recordar pasar siempre la función a sí misma como primer argumento. Bueno, el combinador Y se basa en el combinador U y elimina esa parte tediosa. Esto es bueno porque eliminar / reducir la complejidad es la razón principal por la que hacemos funciones
Primero, derivemos nuestro propio combinador 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)))
Ahora veremos cómo se compara su uso con el combinador U. Observe, para recurrir, en lugar de U (f)
simplemente podemos llamarf ()
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
Y (f => (console.log ('stack overflow imminent!'), f ()))
Ahora demostraré el countDown
uso del programa Y
: verá que los programas son casi idénticos, pero el combinador Y mantiene las cosas un poco más limpias
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
Y ahora veremos factorial
también
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
Como puede ver, se f
convierte en el propio mecanismo de recursividad. Para recurrir, lo llamamos como una función ordinaria. Podemos llamarlo varias veces con diferentes argumentos y el resultado seguirá siendo correcto. Y dado que es un parámetro de función ordinario, podemos nombrarlo como queramos, como a recur
continuación:
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
Combinador U e Y con más de 1 parámetro
En los ejemplos anteriores, vimos cómo podemos hacer un bucle y pasar un argumento para realizar un seguimiento del "estado" de nuestro cálculo. Pero, ¿y si necesitamos realizar un seguimiento del estado adicional?
Nosotros podríamos utilizar los datos de compuestos como una matriz o algo ...
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
Pero esto es malo porque expone el estado interno (contadores a
y b
). Sería bueno si pudiéramos llamar fibonacci (7)
para obtener la respuesta que queremos.
Usando lo que sabemos sobre funciones curry (secuencias de funciones unarias (1 parámetro)), podemos lograr nuestro objetivo fácilmente sin tener que modificar nuestra definición Y
o depender de datos compuestos o características avanzadas del lenguaje.
Mira la definición de de fibonacci
cerca a continuación. Estamos solicitando de inmediato 0
y 1
que están vinculados a a
y b
respectivamente. Ahora, fibonacci simplemente está esperando que se proporcione el último argumento al que se vinculará x
. Cuando recurrimos, debemos llamar f (a) (b) (x)
(no f (a,b,x)
) porque nuestra función está en forma de 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
Este tipo de patrón puede resultar útil para definir todo tipo de funciones. A continuación veremos dos funciones más definidas mediante el Y
combinador ( range
y reduce
) y un derivado de 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 ]
TODO ES ANÓNIMO OMG
Como aquí estamos trabajando con funciones puras, podemos sustituir su definición por cualquier función nombrada. Observe lo que sucede cuando tomamos fibonacci y reemplazamos funciones nombradas con sus expresiones
/* 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
Y ahí lo tiene: fibonacci (7)
calculado de forma recursiva utilizando nada más que funciones anónimas