Combinatore U.
Passando una funzione a se stessa come argomento, una funzione può ricorrere utilizzando il suo parametro invece del suo nome! Quindi la funzione assegnata a U
dovrebbe avere almeno un parametro che si legherà alla funzione (stessa).
Nell'esempio seguente, non abbiamo condizioni di uscita, quindi eseguiremo un ciclo indefinito fino a quando non si verificherà un overflow dello stack
const U = f => f (f) // call function f with itself as an argument
U (f => (console.log ('stack overflow imminent!'), U (f)))
Possiamo fermare la ricorsione infinita usando una varietà di tecniche. Qui scriverò la nostra funzione anonima per restituire un'altra funzione anonima che attende un input; in questo caso, un certo numero. Quando viene fornito un numero, se è maggiore di 0, continueremo a ricorrere, altrimenti restituiremo 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
Ciò che non è immediatamente evidente qui è che la nostra funzione, quando applicata per la prima volta a se stessa utilizzando il U
combinatore, restituisce una funzione in attesa del primo input. Se diamo un nome a questo, possiamo costruire efficacemente funzioni ricorsive usando lambda (funzioni anonime)
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 che questa non è ricorsione diretta , una funzione che chiama se stessa usando il proprio nome. La nostra definizione di countDown
non si riferisce a se stessa all'interno del suo corpo ed è comunque possibile la ricorsione
// 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)
Come rimuovere l'autoriferimento da una funzione esistente utilizzando il combinatore U.
Qui ti mostrerò come prendere una funzione ricorsiva che usa un riferimento a se stessa e cambiarla in una funzione che impiega il combinatore U al posto dell'autore riferimento
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
Ora usando il combinatore U per sostituire il riferimento interno 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
Il modello di sostituzione di base è questo. Prendi nota mentalmente, useremo una strategia simile nella prossima sezione
// self reference recursion
const foo = x => ... foo (nextX) ...
// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)
Combinatore Y.
correlati: i combinatori U e Y spiegati usando un'analogia speculare
Nella sezione precedente abbiamo visto come trasformare la ricorsione autoreferenziale in una funzione ricorsiva che non si basa su una funzione denominata utilizzando il combinatore U. C'è un po 'di fastidio anche se doversi ricordare di passare sempre la funzione a se stessa come primo argomento. Bene, il combinatore Y si basa sul combinatore U e rimuove quel pezzo noioso. Questa è una buona cosa perché rimuovere / ridurre la complessità è il motivo principale per cui creiamo funzioni
Innanzitutto, deriviamo il nostro combinatore Y molto personale
// 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)))
Ora vedremo come il suo utilizzo si confronta con il combinatore a U. Nota, per ricorrere, invece di U (f)
possiamo semplicemente chiamaref ()
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
Y (f => (console.log ('stack overflow imminent!'), f ()))
Ora mostrerò il countDown
programma che usa Y
: vedrai che i programmi sono quasi identici ma il combinatore Y mantiene le cose un po 'più pulite
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 ora vedremo factorial
anche noi
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
Come puoi vedere, f
diventa il meccanismo stesso della ricorsione. Per ricorrere, lo chiamiamo come una funzione ordinaria. Possiamo chiamarlo più volte con argomenti diversi e il risultato sarà comunque corretto. E poiché è un normale parametro di funzione, possiamo nominarlo come preferiamo, come di recur
seguito -
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
Combinatore U e Y con più di 1 parametro
Negli esempi precedenti, abbiamo visto come possiamo ripetere e passare un argomento per tenere traccia dello "stato" del nostro calcolo. Ma cosa succede se dobbiamo tenere traccia dello stato aggiuntivo?
Abbiamo potuto utilizzare i dati composto come un array o qualcosa del genere ...
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
Ma questo è negativo perché espone lo stato interno (contatori a
e b
). Sarebbe bello se potessimo semplicemente chiamare fibonacci (7)
per ottenere la risposta che vogliamo.
Usando ciò che sappiamo sulle funzioni curry (sequenze di funzioni unarie (1 parametro)), possiamo raggiungere facilmente il nostro obiettivo senza dover modificare la nostra definizione di Y
o fare affidamento su dati composti o funzionalità linguistiche avanzate.
Guarda la definizione di fibonacci
sotto. Ci stiamo candidando immediatamente 0
e 1
che sono vincolati a a
e b
rispettivamente. Ora fibonacci sta semplicemente aspettando che venga fornito l'ultimo argomento a cui sarà legato x
. Quando ricorriamo, dobbiamo chiamare f (a) (b) (x)
(non f (a,b,x)
) perché la nostra funzione è in forma curata.
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
Questo tipo di pattern può essere utile per definire tutti i tipi di funzioni. Di seguito vedremo altre due funzioni definite utilizzando il Y
combinatore ( range
e reduce
) e un derivato di 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 ]
È TUTTO ANONIMO OMG
Poiché qui stiamo lavorando con funzioni pure, possiamo sostituire qualsiasi funzione denominata per la sua definizione. Guarda cosa succede quando prendiamo Fibonacci e sostituiamo le funzioni con nome con le loro espressioni
/* 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
Ed ecco fatto: fibonacci (7)
calcolato in modo ricorsivo utilizzando nient'altro che funzioni anonime