DOBRZE!
Poniższy kod jest napisany przy użyciu składni ES6, ale można go równie łatwo napisać w ES5 lub nawet mniej. ES6 nie jest wymagane, aby utworzyć „mechanizm pętli x razy”
Jeśli nie potrzebujesz iteratora w wywołaniu zwrotnym , jest to najprostsza implementacja
const times = x => f => {
if (x > 0) {
f()
times (x - 1) (f)
}
}
// use it
times (3) (() => console.log('hi'))
// or define intermediate functions for reuse
let twice = times (2)
// twice the power !
twice (() => console.log('double vision'))
Jeśli potrzebujesz iteratora , możesz użyć nazwanej funkcji wewnętrznej z parametrem licznika, aby wykonać iterację
const times = n => f => {
let iter = i => {
if (i === n) return
f (i)
iter (i + 1)
}
return iter (0)
}
times (3) (i => console.log(i, 'hi'))
Przestań czytać tutaj, jeśli nie lubisz uczyć się więcej rzeczy ...
Ale coś powinno czuć w tych ...
if
Instrukcje pojedynczej gałęzi są brzydkie - co się dzieje w drugiej gałęzi?
- wiele instrukcji / wyrażeń w ciałach funkcyjnych - czy obawy dotyczące procedury są mieszane?
- niejawnie zwrócone
undefined
- wskazanie nieczystej funkcji powodującej skutki uboczne
"Czy nie ma lepszego sposobu?"
Jest. Przyjrzyjmy się najpierw naszej początkowej implementacji
// times :: Int -> (void -> void) -> void
const times = x => f => {
if (x > 0) {
f() // has to be side-effecting function
times (x - 1) (f)
}
}
Jasne, to proste, ale zwróć uwagę, jak po prostu dzwonimy f()
i nic z tym nie robimy. To naprawdę ogranicza rodzaj funkcji, którą możemy powtarzać wielokrotnie. Nawet jeśli mamy dostępny iterator, f(i)
nie jest dużo bardziej wszechstronny.
A jeśli zaczniemy od lepszego rodzaju procedury powtarzania funkcji? Może coś, co lepiej wykorzystuje dane wejściowe i wyjściowe.
Powtarzanie funkcji ogólnej
// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
if (n > 0)
return repeat (n - 1) (f) (f (x))
else
return x
}
// power :: Int -> Int -> Int
const power = base => exp => {
// repeat <exp> times, <base> * <x>, starting with 1
return repeat (exp) (x => base * x) (1)
}
console.log(power (2) (8))
// => 256
Powyżej zdefiniowaliśmy funkcję ogólną, repeat
która pobiera dodatkowe dane wejściowe, które są używane do rozpoczęcia wielokrotnego stosowania pojedynczej funkcji.
// repeat 3 times, the function f, starting with x ...
var result = repeat (3) (f) (x)
// is the same as ...
var result = f(f(f(x)))
Wdrażanie times
zrepeat
Cóż, teraz jest to łatwe; prawie cała praca jest już wykonana.
// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
if (n > 0)
return repeat (n - 1) (f) (f (x))
else
return x
}
// times :: Int -> (Int -> Int) -> Int
const times = n=> f=>
repeat (n) (i => (f(i), i + 1)) (0)
// use it
times (3) (i => console.log(i, 'hi'))
Ponieważ nasza funkcja przyjmuje i
dane wejściowe i zwraca i + 1
, działa to skutecznie jako nasz iterator, do którego przechodzimy za f
każdym razem.
Naprawiliśmy również naszą listę punktowaną problemów
- Nigdy więcej brzydkich pojedynczych
if
instrukcji
- Treści o pojedynczym wyrażeniu wskazują na ładnie rozdzielone problemy
- Nigdy więcej bezużytecznych, pośrednio zwróconych
undefined
Operator przecinka JavaScript,
Jeśli nie możesz zobaczyć, jak działa ostatni przykład, zależy to od twojej świadomości jednego z najstarszych toporów bojowych JavaScript; operator przecinek - krótko mówiąc, ocenia wyrażenia od lewej do prawej i zwraca wartość ostatniego wyrażenia ocenianej
(expr1 :: a, expr2 :: b, expr3 :: c) :: c
W naszym powyższym przykładzie używam
(i => (f(i), i + 1))
co jest tylko zwięzłym sposobem pisania
(i => { f(i); return i + 1 })
Tail Call Optimization
Choć rekurencyjne implementacje są seksowne, w tym momencie byłoby nieodpowiedzialne, gdybym je polecał, biorąc pod uwagę, że żadna maszyna wirtualna JavaScript , o której myślę, nie obsługuje prawidłowej eliminacji wywołań ogonowych - do transpozycji używano babel, ale jest w stanie "zepsuty; zostanie ponownie zaimplementowany status przez ponad rok.
repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded
W związku z tym powinniśmy ponownie przyjrzeć się naszej implementacji, repeat
aby była bezpieczna dla stosu.
Poniższy kod nie używać zmiennych zmienny n
i x
należy jednak pamiętać, że wszystkie mutacje są zlokalizowane w repeat
funkcji - bez zmiany stanu (mutacje) są widoczne z zewnątrz funkcji
// repeat :: Int -> (a -> a) -> (a -> a)
const repeat = n => f => x =>
{
let m = 0, acc = x
while (m < n)
(m = m + 1, acc = f (acc))
return acc
}
// inc :: Int -> Int
const inc = x =>
x + 1
console.log (repeat (1e8) (inc) (0))
// 100000000
Wiele z was będzie mówić „ale to nie działa!” - Wiem, zrelaksuj się. Możemy zaimplementować interfejs loop
/ styl Clojure recur
do pętli w przestrzeni stałej przy użyciu czystych wyrażeń ; nic z tego while
.
Tutaj abstrahujemy while
od naszej loop
funkcji - szuka ona specjalnego recur
typu, aby utrzymać działanie pętli. Gdy recur
napotkany zostanie inny typ, pętla jest zakończona i zwracany jest wynik obliczenia
const recur = (...args) =>
({ type: recur, args })
const loop = f =>
{
let acc = f ()
while (acc.type === recur)
acc = f (...acc.args)
return acc
}
const repeat = $n => f => x =>
loop ((n = $n, acc = x) =>
n === 0
? acc
: recur (n - 1, f (acc)))
const inc = x =>
x + 1
const fibonacci = $n =>
loop ((n = $n, a = 0, b = 1) =>
n === 0
? a
: recur (n - 1, b, a + b))
console.log (repeat (1e7) (inc) (0)) // 10000000
console.log (fibonacci (100)) // 354224848179262000000