Kombinator U
Przekazując sobie funkcję jako argument, funkcja może powtarzać się przy użyciu swojego parametru zamiast nazwy! Zatem funkcja podana do U
powinna mieć co najmniej jeden parametr, który będzie wiązał się z funkcją (samą).
W poniższym przykładzie nie mamy warunku wyjścia, więc będziemy po prostu pętli w nieskończoność, aż nastąpi przepełnienie stosu
const U = f => f (f) // call function f with itself as an argument
U (f => (console.log ('stack overflow imminent!'), U (f)))
Możemy zatrzymać nieskończoną rekurencję za pomocą różnych technik. Tutaj napiszę naszą anonimową funkcję zwracającą inną anonimową funkcję, która czeka na dane wejściowe; w tym przypadku pewna liczba. Po podaniu liczby, jeśli jest ona większa niż 0, będziemy kontynuować powtarzanie, w przeciwnym razie zwrócimy 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
Nie widać tutaj od razu, że nasza funkcja, gdy po raz pierwszy zastosowana do siebie za pomocą U
kombinatora, zwraca funkcję oczekującą na pierwsze wejście. Jeśli nadaliśmy temu nazwę, możemy skutecznie konstruować funkcje rekurencyjne przy użyciu lambd (funkcji anonimowych)
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
Tyle że to nie jest bezpośrednia rekursja - funkcja, która wywołuje samą siebie używając własnej nazwy. Nasza definicja countDown
nie odwołuje się do siebie w swoim ciele i nadal możliwa jest rekurencja
// 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)
Jak usunąć samoodniesienie z istniejącej funkcji za pomocą kombinatora U.
Tutaj pokażę ci, jak wziąć funkcję rekurencyjną, która używa odwołania do samej siebie i zmienić ją na funkcję, która używa kombinatora U do zamiast odniesienia do siebie
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
Teraz używamy kombinatora U, aby zastąpić odwołanie wewnętrzne do factorial
const U = f => f (f)
const factorial = U (f => x =>
x === 0 ? 1 : x * U (f) (x - 1))
console.log (factorial (5)) // 120
Podstawowy wzór wymiany jest następujący. Zwróć uwagę, że w następnej sekcji będziemy używać podobnej strategii
// self reference recursion
const foo = x => ... foo (nextX) ...
// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)
Kombinator Y.
powiązane: kombinatory U i Y wyjaśniono za pomocą analogii lustrzanej
W poprzedniej sekcji widzieliśmy, jak przekształcić rekursję z samoodniesieniami w funkcję rekurencyjną, która nie polega na nazwanej funkcji przy użyciu kombinatora U. Jest trochę irytujące, że trzeba pamiętać, aby zawsze przekazywać tę funkcję do siebie jako pierwszy argument. Cóż, kombinator Y opiera się na kombinatorze U i usuwa ten żmudny fragment. To dobrze, ponieważ usuwanie / zmniejszanie złożoności jest głównym powodem, dla którego tworzymy funkcje
Najpierw wyprowadźmy nasz własny kombinator 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)))
Teraz zobaczymy, jak jego użycie wypada w porównaniu z kombinatorem U. Zauważ, aby się powtórzyć, zamiast tego U (f)
możemy po prostu zadzwonić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 ()))
Teraz zademonstruję countDown
program przy użyciu Y
- zobaczysz, że programy są prawie identyczne, ale kombinator Y sprawia, że wszystko jest trochę czystsze
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
A teraz zobaczymy factorial
również
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
Jak widać, sam f
staje się mechanizmem rekursji. Aby się powtarzać, nazywamy to zwykłą funkcją. Możemy to wywołać wiele razy z różnymi argumentami, a wynik nadal będzie poprawny. A ponieważ jest to zwykły parametr funkcji, możemy nazwać go dowolnie, na przykład recur
poniżej -
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
Kombinator U i Y z więcej niż 1 parametrem
W powyższych przykładach widzieliśmy, jak możemy zapętlić i przekazać argument, aby śledzić „stan” naszych obliczeń. Ale co, jeśli musimy śledzić dodatkowy stan?
My mogli korzystać z danych złożonych jak tablica lub coś ...
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
Ale to jest złe, ponieważ ujawnia stan wewnętrzny (liczniki a
i b
). Byłoby miło, gdybyśmy mogli po prostu zadzwonić, fibonacci (7)
aby uzyskać odpowiedź, której szukamy.
Korzystając z tego, co wiemy o funkcjach curried (sekwencje funkcji jednoargumentowych (1-paramter)), możemy łatwo osiągnąć nasz cel bez konieczności modyfikowania naszej definicji Y
danych złożonych lub zaawansowanych funkcji językowych lub polegania na nich.
Przyjrzyj się fibonacci
bliżej definicji poniżej. Natychmiast aplikujemy 0
i 1
które są zobowiązane do a
i b
odpowiednio. Teraz fibonacci po prostu czeka na podanie ostatniego argumentu, do którego zostanie przypisany x
. Kiedy powtarzamy, musimy wywołać f (a) (b) (x)
(nie f (a,b,x)
), ponieważ nasza funkcja ma postać 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
Ten rodzaj wzorca może być przydatny do definiowania różnego rodzaju funkcji. Poniżej widzimy dwie kolejne funkcje zdefiniowane za pomocą Y
syntezatora ( range
i reduce
) i pochodną 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 ]
TO WSZYSTKO ANONIMOWE OMG
Ponieważ pracujemy tutaj z czystymi funkcjami, możemy zastąpić dowolną nazwaną funkcję jej definicją. Zobacz, co się dzieje, gdy bierzemy Fibonacciego i zastępujemy nazwane funkcje ich wyrażeniami
/* 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
I fibonacci (7)
gotowe - wyliczane rekurencyjnie, używając wyłącznie funkcji anonimowych