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.