Existe um mecanismo para fazer um loop x vezes no ES6 (ECMAScript 6) sem variáveis ​​mutáveis?


157

A maneira típica de repetir os xtempos no JavaScript é:

for (var i = 0; i < x; i++)
  doStuff(i);

Mas não quero usar o ++operador ou ter nenhuma variável mutável. Então, existe uma maneira, no ES6, de repetir os xtempos de outra maneira? Eu amo o mecanismo de Ruby:

x.times do |i|
  do_stuff(i)
end

Algo semelhante no JavaScript / ES6? Eu poderia meio que trapacear e criar meu próprio gerador:

function* times(x) {
  for (var i = 0; i < x; i++)
    yield i;
}

for (var i of times(5)) {
  console.log(i);
}

Claro que ainda estou usando i++. Pelo menos está fora de vista :), mas espero que exista um mecanismo melhor no ES6.


3
Por que a variável de controle de loop mutável é um problema? Apenas um princípio?
— doldt

1
@doldt - Estou tentando ensinar JavaScript, mas estou tentando adiar o conceito de variáveis ​​mutáveis ​​até mais tarde
— às.

5
Estamos ficando muito off-topic aqui, mas você tem certeza de que passar para geradores ES6 (ou qualquer outro conceito novo, de alto nível) é uma boa idéia antes de aprender sobre variáveis mutáveis :)?
— doldt

5
@ Doldt - talvez, eu estou experimentando. Adotando uma abordagem de linguagem funcional para JavaScript.
— às.

Use let para declarar essa variável no loop. Seu escopo termina com o loop.
— ncmathsadist

Respostas:


156

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 ...

  • ifdeclaraçõ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 repeatfunçã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 timescomrepeat

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 icomo entrada e retorna i + 1, isso efetivamente funciona como nosso iterador, ao qual passamos a fcada vez.

Também corrigimos nossa lista de problemas

  • Não há mais ifdeclaraçõ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 repeatpara torná-la segura para a pilha.

O código a seguir faz usar variáveis mutáveis ne xmas note que todas as mutações estão localizadas à repeatfunçã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 recurpara loop de espaço constante usando expressões puras ; nada disso while.

Aqui, abstraímos whilenossa loopfunção - ela procura um recurtipo especial para manter o loop em execução. Quando um não- recurtipo é 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


24
Parece complicado demais (estou especialmente confuso com g => g(g)(x)). Existe um benefício de uma função de ordem superior à de uma ordem de primeira ordem, como na minha solução?
— Pavlo 26/05

1
@naomik: obrigado por reservar um tempo para publicar um link. muito apreciado.
— Pineda

1
@ AlfonsoPérez Agradeço a observação. Vou ver se eu posso trabalhar um pouco dica em algum lugar lá ^ _ ^
— Obrigado

1
@naomik Farewell TCO ! Estou devastado.

10
Parece que esta resposta é aceita e bem classificada porque deve ter exigido muito esforço, mas não acho que seja uma boa resposta. A resposta correta para a pergunta é "não". É útil listar uma solução alternativa como você fez, mas logo depois você declara que existe uma maneira melhor. Por que você não coloca essa resposta e remove a pior no topo? Por que você está explicando operadores de vírgula? Por que você traz o Clojure? Por que, em geral, tantas tangentes para uma pergunta com uma resposta de 2 caracteres? Perguntas simples não são apenas uma plataforma para os usuários fazerem uma apresentação sobre alguns fatos interessantes da programação.
— Timofey 'Sasha' Kondrashov

266

Usando o operador ES2015 Spread :

[...Array(n)].map()

const res = [...Array(10)].map((_, i) => {
  return i * 10;
});

// as a one liner
const res = [...Array(10)].map((_, i) => i * 10);

Ou se você não precisar do resultado:

[...Array(10)].forEach((_, i) => {
  console.log(i);
});

// as a one liner
[...Array(10)].forEach((_, i) => console.log(i));

Ou usando o operador ES2015 Array.from :

Array.from(...)

const res = Array.from(Array(10)).map((_, i) => {
  return i * 10;
});

// as a one liner
const res = Array.from(Array(10)).map((_, i) => i * 10);

Observe que, se você apenas precisar de uma sequência repetida, poderá usar String.prototype.repeat .

console.log("0".repeat(10))
// 0000000000

26
Melhor:Array.from(Array(10), (_, i) => i*10)
— Bergi 16/10

6
Essa deve ser a melhor resposta. Então ES6! Muito impressionante!
— Gergely Fehérvári

3
Se você não precisar do iterador (i), poderá excluir a chave e o valor para fazer isso: #[...Array(10)].forEach(() => console.log('looping 10 times');
— Sterling Bourne

9
Então você aloca uma matriz inteira de N elementos apenas para jogá-lo fora?
— Kugel

2
Alguém abordou o comentário anterior de Kugel? Eu estava pensando a mesma coisa
— Arman

37
for (let i of Array(100).keys()) {
    console.log(i)
}

Isso funciona, então isso é ótimo! Mas é um pouco feio no sentido de que é necessário trabalho extra e não é para isso que as Arraychaves são usadas.
— às.

@at. de fato. Mas não tenho certeza se existe um sinônimo de haskell [0..x]no JS mais conciso do que na minha resposta.
— Zerkms

você pode estar certo de que não há nada mais conciso do que isso.
— às.

OK, entendo por que isso funciona, dadas as diferenças entre Array.prototype.keyse Object.prototype.keys, mas com certeza é confuso à primeira vista.
— Mark Reed

1
@cchamberlain com TCO em ES2015 (não implementado em qualquer lugar embora?) pode ser a menos de preocupação, mas, na verdade :-)
— zerkms

29

Eu acho que a melhor solução é usar let:

for (let i=0; i<100; i++) …

Isso criará uma nova ivariável (mutável) para cada avaliação do corpo e garantirá que isso iseja alterado apenas na expressão de incremento na sintaxe do loop, e não em qualquer outro lugar.

Eu poderia meio que trapacear e criar meu próprio gerador. Pelo menos i++está fora de vista :)

Isso deve ser suficiente. Mesmo em idiomas puros, todas as operações (ou pelo menos seus intérpretes) são construídas a partir de primitivos que usam mutação. Contanto que tenha um escopo adequado, não vejo o que há de errado nisso.

Você deveria ficar bem com

function* times(n) {
  for (let i = 0; i < x; i++)
    yield i;
}
for (const i of times(5))
  console.log(i);

Mas não quero usar o ++operador ou ter nenhuma variável mutável.

Então sua única opção é usar recursão. Você pode definir essa função do gerador sem um mutável itambém:

function* range(i, n) {
  if (i >= n) return;
  yield i;
  return yield* range(i+1, n);
}
times = (n) => range(0, n);

Mas isso me parece um exagero e pode ter problemas de desempenho (já que a eliminação de chamadas não está disponível return yield*).


1
Eu gosto dessa opção - agradável e simples!
— 23416 DanV

2
Isto é simples e direto ao ponto e não aloca uma matriz como muitas respostas acima
— Kugel

@Kugel O segundo pode alocar na pilha, embora
— Bergi

Bom ponto de não tenho certeza se a otimização de chamada vai funcionar aqui @Bergi
— Kugel



11

Resposta: 09 de dezembro de 2015

Pessoalmente, achei a resposta aceita concisa (boa) e concisa (ruim). Aprecie que esta declaração possa ser subjetiva. Leia esta resposta e veja se você concorda ou discorda.

O exemplo dado na pergunta era algo como Ruby:

x.times do |i|
  do_stuff(i)
end

Expressar isso em JS usando abaixo permitiria:

times(x)(doStuff(i));

Aqui está o código:

let times = (n) => {
  return (f) => {
    Array(n).fill().map((_, i) => f(i));
  };
};

É isso aí!

Exemplo de uso simples:

let cheer = () => console.log('Hip hip hooray!');

times(3)(cheer);

//Hip hip hooray!
//Hip hip hooray!
//Hip hip hooray!

Como alternativa, siga os exemplos da resposta aceita:

let doStuff = (i) => console.log(i, ' hi'),
  once = times(1),
  twice = times(2),
  thrice = times(3);

once(doStuff);
//0 ' hi'

twice(doStuff);
//0 ' hi'
//1 ' hi'

thrice(doStuff);
//0 ' hi'
//1 ' hi'
//2 ' hi'

Nota lateral - Definindo uma função de faixa

Uma pergunta semelhante / relacionada, que usa construções de código fundamentalmente muito semelhantes, pode ser que exista uma função Range conveniente no JavaScript (principal), algo semelhante à função range do sublinhado.

Crie uma matriz com n números, começando com x

Sublinhado

_.range(x, x + n)

ES2015

Duas alternativas:

Array(n).fill().map((_, i) => x + i)

Array.from(Array(n), (_, i) => x + i)

Demonstração usando n = 10, x = 1:

> Array(10).fill().map((_, i) => i + 1)
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

> Array.from(Array(10), (_, i) => i + 1)
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

Em um teste rápido, eu executei, com cada uma das opções acima executando um milhão de vezes cada uma, usando nossa solução e função doStuff, a abordagem anterior (Array (n) .fill ()) se mostrou um pouco mais rápida.


8
Array(100).fill().map((_,i)=> console.log(i) );

Esta versão atende aos requisitos de imutabilidade do OP. Considere também usar em reducevez de map depender do seu caso de uso.

Essa também é uma opção se você não se importar com uma pequena mutação no seu protótipo.

Number.prototype.times = function(f) {
   return Array(this.valueOf()).fill().map((_,i)=>f(i));
};

Agora podemos fazer isso

((3).times(i=>console.log(i)));

+1 para arcseldon para a .fillsugestão.


Votado para baixo, como método de preenchimento não é suportado no IE ou Opera ou PhantomJS
— morhook

8

Aqui está outra boa alternativa:

Array.from({ length: 3}).map(...);

De preferência, como o @Dave Morse apontou nos comentários, você também pode se livrar da mapchamada usando o segundo parâmetro da Array.fromfunção da seguinte maneira:

Array.from({ length: 3 }, () => (...))


2
Essa deve ser a resposta aceita! Uma pequena sugestão - você já obtém a funcionalidade de mapa que precisa gratuitamente com o Array.from: Array.from({ length: label.length }, (_, i) => (...)) Isso economiza a criação de um array temporário vazio apenas para iniciar uma chamada para o mapa.
— Dave Morse

7

Não é algo que eu ensinaria (ou nunca usaria no meu código), mas aqui está uma solução digna de codegolf sem alterar uma variável, sem a necessidade do ES6:

Array.apply(null, {length: 10}).forEach(function(_, i){
    doStuff(i);
})

Mais uma coisa interessante de prova de conceito do que uma resposta útil, na verdade.


Não pode Array.apply(null, {length: 10})ser justo Array(10)?
— Pavlo

1
@ Pavlo, na verdade, não. A matriz (10) criaria uma matriz de comprimento 10, mas sem nenhuma chave definida, o que torna a construção forEach não utilizável nesse caso. Mas, de fato, isso pode ser simplificado se você não usar forEach, veja a resposta de zerkms (que usa o ES6!).
— doldt

creative @doldt, mas estou procurando algo fácil de aprender e simples.
— às.

5

Estou atrasado para a festa, mas como essa pergunta aparece com frequência nos resultados de pesquisa, gostaria de adicionar uma solução que considero a melhor em termos de legibilidade e por não ser longa (o que é ideal para qualquer IMO da base de código) . Ele sofre mutações, mas eu faria essa troca pelos princípios do KISS.

let times = 5
while( times-- )
    console.log(times)
// logs 4, 3, 2, 1, 0

3
Obrigado por ser a voz da razão no que só posso descrever como uma festa fetichista de lambda de ordem superior. Eu também terminei nesta sessão de perguntas e respostas, seguindo um caminho inócuo do primeiro hit no Google e rapidamente tive minha sanidade profanada pela maioria das respostas aqui. O seu é o primeiro da lista que eu consideraria uma solução direta para um problema direto.
— Martin Devillers

O único problema disso é que é um pouco contra-intuitivo se você deseja usar a timesvariável dentro do loop. Talvez countdownfosse um nome melhor. Caso contrário, a resposta mais limpa e clara da página.
— Tony Brasunas 20/07

3

Afaik, não há mecanismo no ES6 semelhante ao timesmétodo Ruby . Mas você pode evitar a mutação usando a recursão:

let times = (i, cb, l = i) => {
  if (i === 0) return;

  cb(l - i);
  times(i - 1, cb, l);
}

times(5, i => doStuff(i));

Demonstração: http://jsbin.com/koyecovano/1/edit?js,console


Eu gosto dessa abordagem, adoro recursão. Mas eu adoraria algo mais simples para mostrar novos loops de usuários de JavaScript.
— às.

3

Se você deseja usar uma biblioteca, também há lodash_.times ou sublinhado_.times :

_.times(x, i => {
   return doStuff(i)
})

Observe que isso retorna uma matriz de resultados, por isso é realmente mais parecido com este ruby:

x.times.map { |i|
  doStuff(i)
}

2

No paradigma funcional repeaté geralmente uma função recursiva infinita. Para usá-lo, precisamos de um estilo de avaliação lenta ou de aprovação de continuação.

Repetição preguiçosa da função avaliada

const repeat = f => x => [x, () => repeat(f) (f(x))];
const take = n => ([x, f]) => n === 0 ? x : take(n - 1) (f());

console.log(
  take(8) (repeat(x => x * 2) (1)) // 256
);

Eu uso um thunk (uma função sem argumentos) para obter uma avaliação lenta em Javascript.

Repetição de funções com estilo de passagem contínua

const repeat = f => x => [x, k => k(repeat(f) (f(x)))];
const take = n => ([x, k]) => n === 0 ? x : k(take(n - 1));

console.log(
  take(8) (repeat(x => x * 2) (1)) // 256
);

CPS é um pouco assustador no começo. No entanto, ele sempre segue o mesmo padrão: O último argumento é a continuação (a função), que invoca seu próprio corpo: k => k(...). Observe que o CPS inverte o aplicativo, ou seja, take(8) (repeat...)torna-se k(take(8)) (...)onde ké o parcialmente aplicado repeat.

Conclusão

Ao separar a repetição ( repeat) da condição de terminação ( take), obtemos flexibilidade - separação de preocupações até seu amargo fim: D


1

Vantagens desta solução

  • Mais simples de ler / usar (imo)
  • O valor de retorno pode ser usado como uma soma ou apenas ignorado
  • Versão simples do es6, também vincule à versão do código TypeScript

Desvantagens - Mutação. Sendo apenas interno, eu não me importo, talvez alguns outros também não.

Exemplos e Código

times(5, 3)                       // 15    (3+3+3+3+3)

times(5, (i) => Math.pow(2,i) )   // 31    (1+2+4+8+16)

times(5, '<br/>')                 // <br/><br/><br/><br/><br/>

times(3, (i, count) => {          // name[0], name[1], name[2]
    let n = 'name[' + i + ']'
    if (i < count-1)
        n += ', '
    return n
})

function times(count, callbackOrScalar) {
    let type = typeof callbackOrScalar
    let sum
    if (type === 'number') sum = 0
    else if (type === 'string') sum = ''

    for (let j = 0; j < count; j++) {
        if (type === 'function') {
            const callback = callbackOrScalar
            const result = callback(j, count)
            if (typeof result === 'number' || typeof result === 'string')
                sum = sum === undefined ? result : sum + result
        }
        else if (type === 'number' || type === 'string') {
            const scalar = callbackOrScalar
            sum = sum === undefined ? scalar : sum + scalar
        }
    }
    return sum
}

Versão TypeScipt
https://codepen.io/whitneyland/pen/aVjaaE?editors=0011


0

abordando o aspecto funcional:

function times(n, f) {
    var _f = function (f) {
        var i;
        for (i = 0; i < n; i++) {
            f(i);
        }
    };
    return typeof f === 'function' && _f(f) || _f;
}
times(6)(function (v) {
    console.log('in parts: ' + v);
});
times(6, function (v) {
    console.log('complete: ' + v);
});

5
"abordando o aspecto funcional" e, em seguida, usando um loop imperativo com um mutável i. Qual é a razão para usar até timesmais velho do que fornunca?
— Zerkms

reutilizar como var twice = times(2);.
— Nina Scholz

Então, por que não usar apenas forduas vezes?
— Zerkms 26/05

Eu não tenho medo de usar. a questão era algo para não usar uma variável. mas o resultado é sempre algum tipo de cache, também conhecido como variável.
— Nina Scholz

1
"era algo para não usar uma variável" - e você ainda a usa - i++. Não é óbvio como envolver algo inaceitável em uma função a torna melhor.
— Zerkms

0

Geradores? Recursão? Por que tanto odiando mutantes? ;-)

Se for aceitável desde que o "oculte", aceite o uso de um operador unário e podemos manter as coisas simples :

Number.prototype.times = function(f) { let n=0 ; while(this.valueOf() > n) f(n++) }

Assim como no ruby:

> (3).times(console.log)
0
1
2

2
Polegares para cima: "Por que tanto odiar mutantes?"
— 22417 Sarreph

1
Polegar para cima por simplicidade, polegar para baixo por ficar um pouco rubi demais com o monkeypatch. Apenas diga não àqueles macacos maus e maus.
— Mrm 2/17

1
@mrm é este "patch de macaco", não é apenas um caso de extensão? Abraçar e estender :)
— conny

Não. A adição de funções a Number (ou String ou Array ou qualquer outra classe que você não criou) é, por definição, polyfills ou monkey patches - e até mesmo polyfills não são recomendados. Leia as definições de "patch de macaco", "polyfill" e uma alternativa recomendada, "ponyfill". É isso que você quer.
— Mrm 31/05/19

Para estender Number, você faria: class SuperNumber estende Number {times (fn) {for (let i = 0; i <this; i ++) {fn (i); }}
— Alexander

0

Embrulhei a resposta do @Tieme com uma função auxiliar.

No TypeScript:

export const mapN = <T = any[]>(count: number, fn: (...args: any[]) => T): T[] => [...Array(count)].map((_, i) => fn())

Agora você pode executar:

const arr: string[] = mapN(3, () => 'something')
// returns ['something', 'something', 'something']

0

Eu fiz isso:

function repeat(func, times) {
    for (var i=0; i<times; i++) {
        func(i);
    }
}

Uso:

repeat(function(i) {
    console.log("Hello, World! - "+i);
}, 5)

/*
Returns:
Hello, World! - 0
Hello, World! - 1
Hello, World! - 2
Hello, World! - 3
Hello, World! - 4
*/

A ivariável retorna a quantidade de vezes que foi repetida - útil se você precisar pré-carregar uma quantidade x de imagens.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.