ESTÁ BEM!
O código abaixo é escrito usando as sintaxes do ES6, mas poderia ser facilmente escrito no ES5 ou até menos. O ES6 não é um requisito para criar um "mecanismo para repetir x vezes"
Se você não precisar do iterador no retorno de chamada , esta é a implementação mais simples
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'))
Se você precisar do iterador , poderá usar uma função interna nomeada com um parâmetro counter para iterar para você
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'))
Pare de ler aqui se você não gosta de aprender mais coisas ...
Mas algo deve estar errado com aqueles ...
if
declarações de ramo único são feias - o que acontece no outro ramo?
- múltiplas declarações / expressões nos corpos das funções - as preocupações com procedimentos estão sendo misturadas?
- retornado implicitamente
undefined
- indicação de função impura e com efeitos colaterais
"Não existe uma maneira melhor?"
Há sim. Vamos revisitar primeiro nossa implementação 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, é simples, mas observe como ligamos f()
e não fazemos nada com isso. Isso realmente limita o tipo de função que podemos repetir várias vezes. Mesmo que tenhamos o iterador disponível, f(i)
não é muito mais versátil.
E se começarmos com um tipo melhor de procedimento de repetição de função? Talvez algo que faça melhor uso de entrada e saída.
Repetição de função genérica
// 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
Acima, definimos uma repeat
função genérica que recebe uma entrada adicional que é usada para iniciar a aplicação repetida de uma única função.
// 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
comrepeat
Bem, isso é fácil agora; quase todo o trabalho já está feito.
// 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'))
Como nossa função assume i
como entrada e retorna i + 1
, isso efetivamente funciona como nosso iterador, ao qual passamos a f
cada vez.
Também corrigimos nossa lista de problemas
- Não há mais
if
declarações de ramo único feias
- Os corpos de expressão única indicam preocupações bem separadas
- Não é mais inútil, retornou implicitamente
undefined
Operador de vírgula JavaScript, o
Caso esteja com problemas para ver como o último exemplo está funcionando, isso depende da sua consciência de um dos eixos de batalha mais antigos do JavaScript; o operador vírgula - em resumo, avalia expressões da esquerda para a direita e retorna o valor da última expressão avaliada
(expr1 :: a, expr2 :: b, expr3 :: c) :: c
No exemplo acima, estou usando
(i => (f(i), i + 1))
que é apenas uma maneira sucinta de escrever
(i => { f(i); return i + 1 })
Otimização de chamada de cauda
Por mais sexy que sejam as implementações recursivas, neste momento seria irresponsável para mim recomendá-las, já que nenhuma VM JavaScript que eu possa imaginar suporta a eliminação adequada de chamadas de cauda - o babel usado para transpilar, mas está "quebrado; será reimplementado" "status por mais de um ano.
repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded
Como tal, devemos revisitar nossa implementação repeat
para torná-la segura para a pilha.
O código a seguir faz usar variáveis mutáveis n
e x
mas note que todas as mutações estão localizadas à repeat
função - nenhuma alteração de estado (mutações) são visíveis a partir do exterior da função
// 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
Muitos vão dizer "mas isso não é funcional!" Eu sei, apenas relaxe. Podemos implementar uma interface loop
/ estilo Clojure recur
para loop de espaço constante usando expressões puras ; nada disso while
.
Aqui, abstraímos while
nossa loop
função - ela procura um recur
tipo especial para manter o loop em execução. Quando um não- recur
tipo é encontrado, o loop é concluído e o resultado da computação é retornado.
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