¡OKAY!
El siguiente código está escrito con sintaxis de ES6 pero podría escribirse con la misma facilidad en ES5 o incluso menos. ES6 no es un requisito para crear un "mecanismo para repetir x veces"
Si no necesita el iterador en la devolución de llamada , esta es la implementación más simple
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'))
Si necesita el iterador , puede usar una función interna con nombre con un parámetro de contador para iterar por usted
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'))
Deja de leer aquí si no te gusta aprender más cosas ...
Pero algo debería sentirse mal sobre esos ...
- las
if
declaraciones de una sola rama son feas, ¿qué sucede en la otra rama?
- múltiples declaraciones / expresiones en los cuerpos de la función: ¿ se mezclan las inquietudes del procedimiento?
- devuelto implícitamente
undefined
- indicación de función impura, efecto secundario
"¿No hay una mejor manera?"
Ahi esta. Primero revisemos nuestra implementación inicial
// times :: Int -> (void -> void) -> void
const times = x => f => {
if (x > 0) {
f() // has to be side-effecting function
times (x - 1) (f)
}
}
Claro, es simple, pero observe cómo simplemente llamamos f()
y no hacemos nada con él. Esto realmente limita el tipo de función que podemos repetir varias veces. Incluso si tenemos el iterador disponible, f(i)
no es mucho más versátil.
¿Qué pasa si comenzamos con un mejor tipo de procedimiento de repetición de funciones? Quizás algo que haga un mejor uso de la entrada y la salida.
Función genérica de repetición
// 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
Arriba, definimos una repeat
función genérica que toma una entrada adicional que se utiliza para iniciar la aplicación repetida de una sola función.
// 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)))
Implementando times
conrepeat
Bueno, esto es fácil ahora; Casi todo el trabajo ya está hecho.
// 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'))
Dado que nuestra función toma i
como entrada y regresa i + 1
, esto efectivamente funciona como nuestro iterador al que pasamos f
cada vez.
También hemos solucionado nuestra lista de problemas.
- No más
if
declaraciones feas de una sola rama
- Los cuerpos de expresión única indican preocupaciones bien separadas
- No más inútil, devuelto implícitamente
undefined
Operador de coma JavaScript, el
En caso de que tenga problemas para ver cómo funciona el último ejemplo, depende de su conocimiento de uno de los ejes de batalla más antiguos de JavaScript; el operador de coma : en resumen, evalúa las expresiones de izquierda a derecha y devuelve el valor de la última expresión evaluada
(expr1 :: a, expr2 :: b, expr3 :: c) :: c
En nuestro ejemplo anterior, estoy usando
(i => (f(i), i + 1))
que es solo una forma sucinta de escribir
(i => { f(i); return i + 1 })
Tail Call Optimization
A pesar de lo sexy que son las implementaciones recursivas, en este punto sería irresponsable para mí recomendarlas, dado que ninguna máquina virtual JavaScript en la que pueda pensar admite la eliminación adecuada de llamadas de cola: Babel solía transpilarla, pero se ha roto, se volverá a implementar "estado por más de un año.
repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded
Como tal, deberíamos revisar nuestra implementación de repeat
para que sea apilable.
El código siguiente hace utilizar variables mutables n
y x
pero tenga en cuenta que todas las mutaciones se localizan en la repeat
función - no hay cambios de estado (mutaciones) son visibles desde fuera de la función
// 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
Esto hará que muchos de ustedes digan "¡pero eso no es funcional!" - Lo sé, solo relájate. Podemos implementar una interfaz loop
/ estilo Clojure recur
para bucles de espacio constante usando expresiones puras ; Ninguna de esas while
cosas.
Aquí abstraemos while
nuestra loop
función: busca un recur
tipo especial para mantener el ciclo en ejecución. Cuando recur
se encuentra un no tipo, el ciclo finaliza y se devuelve el resultado del cálculo.
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