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 Udeve 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 Ucombinador, 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 countDownnã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 countDownprograma 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 factorialtambé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, ftorna-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 recurabaixo -
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 ae 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 You confiar em dados compostos ou recursos avançados de linguagem.
Veja fibonacciatentamente a definição de abaixo. Estamos aplicando imediatamente 0e 1quais são obrigados ae brespectivamente. 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 Ycombinator ( rangee 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