Usando map () em um iterador

87

Digamos que temos um Mapa :, let m = new Map();usando m.values()retorna um iterador de mapa.

Mas não posso usar forEach()ou map()naquele iterador e implementar um loop while nesse iterador parece um anti-padrão, já que o ES6 oferece funções como map().

Então, há uma maneira de usar map()em um iterador?

Shinzou
fonte
Não pronto para uso, mas você pode usar bibliotecas de terceiros como lodash mapfunções que também oferecem suporte ao mapa.
perigoso
O próprio mapa tem um forEach para iterar seus pares de valores-chave.
perigoso
Converter o iterador em um array e mapear nele Array.from(m.values()).map(...)funciona, mas acho que não é a melhor maneira de fazer isso.
JiminP de
qual problema como você resolver com o uso de um iterador enquanto um array se encaixaria melhor para usar Array#map?
Nina Scholz
1
@NinaScholz Estou usando um conjunto geral como este: stackoverflow.com/a/29783624/4279201
shinzou

Respostas:

80

A maneira mais simples e de menor desempenho de fazer isso é:

Array.from(m).map(([key,value]) => /* whatever */)

Melhor ainda

Array.from(m, ([key, value]) => /* whatever */))

Array.frompega qualquer coisa iterável ou semelhante a um array e o converte em um array! Como Daniel aponta nos comentários, podemos adicionar uma função de mapeamento à conversão para remover uma iteração e, subsequentemente, um array intermediário.

Usar Array.fromirá mover seu desempenho de O(1)para O(n)como @hraban aponta nos comentários. Como mé um Mape eles não podem ser infinitos, não precisamos nos preocupar com uma sequência infinita. Na maioria dos casos, isso será suficiente.

Existem algumas outras maneiras de percorrer um mapa.

Usando forEach

m.forEach((value,key) => /* stuff */ )

Usando for..of

var myMap = new Map();
myMap.set(0, 'zero');
myMap.set(1, 'one');
for (var [key, value] of myMap) {
  console.log(key + ' = ' + value);
}
// 0 = zero
// 1 = one
Ktilcu
fonte
Os mapas podem ter um comprimento infinito?
ktilcu
2
@ktilcu para um iterador: sim. um .map em um iterador pode ser pensado como uma transformação no gerador, que retorna o próprio iterador. popping um elemento chama o iterador subjacente, transforma o elemento e retorna isso.
hraban
7
O problema com essa resposta é que ela transforma o que poderia ser um algoritmo de memória O (1) em O (n), o que é bastante sério para conjuntos de dados maiores. Além de, é claro, exigir iteradores finitos e não transmitíveis. O título da pergunta é "Usando map () em um iterador", discordo que sequências preguiçosas e infinitas não fazem parte da pergunta. É exatamente assim que as pessoas usam os iteradores. O "mapa" era apenas um exemplo ("Diga .."). O bom dessa resposta é sua simplicidade, o que é muito importante.
hraban
1
@hraban Obrigado por adicionar a esta discussão. Posso atualizar a resposta para incluir algumas advertências, apenas para que os futuros viajantes tenham as informações à frente e no centro. No final das contas, frequentemente teremos que tomar a decisão entre desempenho simples e ótimo. Eu geralmente fico do lado mais simples (para depurar, manter, explicar) sobre o desempenho.
ktilcu
3
@ktilcu Em vez disso, você pode chamar Array.from(m, ([key,value]) => /* whatever */)(observe que a função de mapeamento está dentro do from) e, em seguida, nenhum array intermediário é criado ( fonte ). Ele ainda se move de O (1) para O (n), mas pelo menos a iteração e o mapeamento acontecem em apenas uma iteração completa.
Daniel de
18

Você poderia definir outra função iteradora para fazer um loop sobre isso:

function* generator() {
    for(let i = 0; i < 10; i++) {
        console.log(i);
        yield i;
    }
}

function* mapIterator(iterator, mapping) {
    while (true) {
        let result = iterator.next();
        if (result.done) {
            break;
        }
        yield mapping(result.value);
    }
}

let values = generator();
let mapped = mapIterator(values, (i) => {
    let result = i*2;
    console.log(`x2 = ${result}`);
    return result;
});

console.log('The values will be generated right now.');
console.log(Array.from(mapped).join(','));

Agora você pode perguntar: por que não usar Array.from? Uma vez que esta será executada através de toda a iteração, guardá-lo para uma matriz (temporária), iterar-lo novamente e , em seguida, fazer o mapeamento. Se a lista for enorme (ou mesmo potencialmente infinita), isso levará ao uso desnecessário da memória.

Claro, se a lista de itens for bastante pequena, o uso Array.fromdeve ser mais do que suficiente.

Sheean
fonte
Como uma quantidade finita de memória pode conter uma estrutura de dados infinita?
shinzou de
3
não, esse é o ponto. Usando isso, você pode criar "fluxos de dados" encadeando uma fonte de iterador a um monte de transformações de iterador e, finalmente, um coletor de consumo. Por exemplo, para processamento de streaming de áudio, trabalho com arquivos enormes, agregadores em bancos de dados, etc.
hraban
1
Eu gosto dessa resposta. Alguém pode recomendar uma biblioteca que oferece métodos semelhantes a Array em iteráveis?
Joel Malone
1
mapIterator()não garante que o iterador subjacente será devidamente fechado ( iterator.return()chamado), a menos que o próximo valor de retorno tenha sido chamado pelo menos uma vez. Veja: repeater.js.org/docs/safety
Jaka Jančar
11

A maneira mais simples e eficiente é usar o segundo argumento Array.frompara conseguir isso:

const map = new Map()
map.set('a', 1)
map.set('b', 2)

Array.from(map, ([key, value]) => `${key}:${value}`)
// ['a:1', 'b:2']

Essa abordagem funciona para qualquer iterável não infinito . E evita ter que usar uma chamada separada para a Array.from(map).map(...)qual iteraria por meio do iterável duas vezes e seria pior para o desempenho.

Ian Storm Taylor
fonte
3

Você poderia recuperar um iterador sobre o iterável e, em seguida, retornar outro iterador que chama a função de retorno de chamada de mapeamento em cada elemento iterado.

const map = (iterable, callback) => {
  return {
    [Symbol.iterator]() {
      const iterator = iterable[Symbol.iterator]();
      return {
        next() {
          const r = iterator.next();
          if (r.done)
            return r;
          else {
            return {
              value: callback(r.value),
              done: false,
            };
          }
        }
      }
    }
  }
};

// Arrays are iterable
console.log(...map([0, 1, 2, 3, 4], (num) => 2 * num)); // 0 2 4 6 8
MartyO256
fonte
2

Você pode usar itiriri que implementa métodos semelhantes a array para iteráveis:

import { query } from 'itiriri';

let m = new Map();
// set map ...

query(m).filter([k, v] => k < 10).forEach([k, v] => console.log(v));
let arr = query(m.values()).map(v => v * 10).toArray();
Dimadeveatii
fonte
Agradável! É assim que as APIs de JS deveriam ser feitas. Como sempre, Rust acerta: doc.rust-lang.org/std/iter/trait.Iterator.html
ovelhas voadoras
1

Dê uma olhada em https://www.npmjs.com/package/fluent-iterable

Funciona com todos os iteráveis ​​(Mapa, função do gerador, array) e iteráveis ​​assíncronos.

const map = new Map();
...
console.log(fluent(map).filter(..).map(..));
kataik
fonte