Combinador em U
Ao passar uma função para si mesma como argumento, uma função pode se repetir usando seu parâmetro em vez de seu nome! Portanto, a função atribuída U
deve ter pelo menos um parâmetro que se vincule à função (ela mesma).
No exemplo abaixo, não temos condição de saída, portanto, executaremos um loop indefinidamente até que ocorra um estouro de pilha
const U = f => f (f) // call function f with itself as an argument
U (f => (console.log ('stack overflow imminent!'), U (f)))
Podemos parar a recursão infinita usando uma variedade de técnicas. Aqui, escreverei nossa função anônima para retornar outra função anônima que está aguardando uma entrada; neste caso, algum número. Quando um número é fornecido, se for maior que 0, continuaremos recorrendo, caso contrário, retornará 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
O que não é imediatamente aparente aqui é que nossa função, quando aplicada pela primeira vez a si mesma usando o U
combinador, retorna uma função aguardando a primeira entrada. Se dermos um nome a isso, podemos efetivamente construir funções recursivas usando lambdas (funções 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
Só que isso não é recursão direta - uma função que se chama usando seu próprio nome. Nossa definição de countDown
não se refere a si mesma dentro de seu corpo e ainda é possível recursão
// 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)
Como remover a auto-referência de uma função existente usando o combinador U
Aqui, mostrarei como pegar uma função recursiva que usa uma referência para si mesma e alterá-la para uma função que emprega o combinador U no lugar da auto-referência
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
Agora, usando o combinador U para substituir a referência interna para factorial
const U = f => f (f)
const factorial = U (f => x =>
x === 0 ? 1 : x * U (f) (x - 1))
console.log (factorial (5)) // 120
O padrão básico de substituição é esse. Faça uma anotação mental, usaremos uma estratégia semelhante na próxima seção
// self reference recursion
const foo = x => ... foo (nextX) ...
// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)
Combinador Y
relacionados: os combinadores U e Y explicados usando uma analogia de espelho
Na seção anterior, vimos como transformar a recursão de auto-referência em uma função recursiva que não depende de uma função nomeada usando o combinador U. Há um certo aborrecimento em ter que se lembrar de sempre passar a função para si mesma como o primeiro argumento. Bem, o combinador Y se baseia no combinador U e remove esse bit tedioso. Isso é bom porque remover / reduzir a complexidade é a principal razão pela qual fazemos funções
Primeiro, vamos derivar nosso próprio 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)))
Agora veremos como seu uso se compara ao combinador em U. Observe, para repetir, em vez de U (f)
simplesmente chamarmosf ()
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
Y (f => (console.log ('stack overflow imminent!'), f ()))
Agora vou demonstrar o countDown
programa usando Y
- você verá que os programas são quase idênticos, mas o combinador Y mantém as coisas um pouco mais limpas
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
E agora vamos ver factorial
também
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 você pode ver, f
torna-se o mecanismo de recursão em si. Para repetir, chamamos isso de uma função comum. Podemos chamá-lo várias vezes com argumentos diferentes e o resultado ainda estará correto. E como é um parâmetro de função comum, podemos nomear o que quisermos, como recur
abaixo -
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 com mais de 1 parâmetro
Nos exemplos acima, vimos como podemos fazer um loop e passar um argumento para acompanhar o "estado" de nossa computação. Mas e se precisarmos acompanhar o estado adicional?
Nós poderia usar os dados compostos como um Array ou algo assim ...
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
Mas isso é ruim porque está expondo o estado interno (contadores a
e b
). Seria bom se pudéssemos ligar fibonacci (7)
para obter a resposta que queremos.
Usando o que sabemos sobre funções com caril (sequências de funções unárias (1 parâmetro)), podemos alcançar nosso objetivo facilmente sem precisar modificar nossa definição Y
ou confiar em dados compostos ou recursos avançados de linguagem.
Veja fibonacci
atentamente a definição de abaixo. Estamos aplicando imediatamente 0
e 1
quais são obrigados a
e b
respectivamente. Agora, fibonacci está simplesmente aguardando o último argumento a ser fornecido, que será vinculado x
. Quando recessamos, devemos ligar f (a) (b) (x)
(não f (a,b,x)
) porque nossa função está na forma de caril.
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
Esse tipo de padrão pode ser útil para definir todos os tipos de funções. Abaixo veremos mais duas funções definidas usando o Y
combinator ( range
e reduce
) e um 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 ]
É TUDO ANÔNIMO OMG
Como estamos trabalhando com funções puras aqui, podemos substituir qualquer função nomeada por sua definição. Veja o que acontece quando pegamos fibonacci e substituímos funções nomeadas por suas expressões
/* 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
E aí está - fibonacci (7)
calculado recursivamente usando nada além de funções anônimas