Utiliser map () sur un itérateur

88

Supposons que nous ayons une carte:, let m = new Map();using m.values()renvoie un itérateur de carte.

Mais je ne peux pas utiliser forEach()ou map()sur cet itérateur et implémenter une boucle while sur cet itérateur semble être un anti-pattern puisque ES6 offre des fonctions comme map().

Alors, y a-t-il un moyen d'utiliser map()sur un itérateur?

Shinzou
la source
Pas hors de la boîte, mais vous pouvez utiliser des bibliothèques tierces comme la lodash mapfonction qui prend également en charge Map.
dangereux
La carte elle-même a un forEach pour itérer sur ses paires clé-valeur.
dangereux
Convertir l'itérateur en tableau et mapper dessus Array.from(m.values()).map(...)fonctionne, mais je pense que ce n'est pas la meilleure façon de le faire.
JiminP
quel problème aimez-vous résoudre avec l'utilisation d'un itérateur alors qu'un tableau conviendrait mieux à l'utilisation Array#map?
Nina Scholz
1
@NinaScholz J'utilise un ensemble général comme ici: stackoverflow.com/a/29783624/4279201
shinzou

Réponses:

81

Le moyen le plus simple et le moins performant de le faire est:

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

Mieux encore

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

Array.fromprend n'importe quel élément itérable ou semblable à un tableau et le convertit en un tableau! Comme Daniel le souligne dans les commentaires, nous pouvons ajouter une fonction de mappage à la conversion pour supprimer une itération et par la suite un tableau intermédiaire.

L'utilisation Array.fromdéplacera votre performance de O(1)à O(n)comme @hraban le souligne dans les commentaires. Puisque mc'est a Map, et qu'ils ne peuvent pas être infinis, nous n'avons pas à nous soucier d'une séquence infinie. Dans la plupart des cas, cela suffira.

Il existe plusieurs autres façons de parcourir une carte.

En utilisant forEach

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

En utilisant 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
la source
Les cartes peuvent-elles avoir une longueur infinie?
ktilcu
2
@ktilcu pour un itérateur: oui. un .map sur un itérateur peut être considéré comme une transformation sur le générateur, qui retourne lui-même un itérateur. popping one element appelle l'itérateur sous-jacent, transforme l'élément et le renvoie.
hraban
7
Le problème avec cette réponse est qu'elle transforme ce qui pourrait être un algorithme de mémoire O (1) en un algorithme O (n), ce qui est assez sérieux pour des ensembles de données plus volumineux. Mis à part, bien sûr, des itérateurs finis et non streamables. Le titre de la question est "Utilisation de map () sur un itérateur", je ne suis pas d'accord avec le fait que les séquences paresseuses et infinies ne font pas partie de la question. C'est précisément ainsi que les gens utilisent les itérateurs. La "carte" n'était qu'un exemple ("Dites ..."). La bonne chose à propos de cette réponse est sa simplicité, ce qui est très important.
hraban
1
@hraban Merci d'avoir participé à cette discussion. Je peux mettre à jour la réponse pour inclure quelques mises en garde afin que les futurs voyageurs aient les informations à l'avant-plan. En fin de compte, nous devrons souvent faire le choix entre des performances simples et optimales. Je vais généralement du côté des performances plus simples (pour déboguer, maintenir, expliquer).
ktilcu
3
@ktilcu Vous pouvez à la place appeler Array.from(m, ([key,value]) => /* whatever */)(notez que la fonction de mappage est à l'intérieur de from) et aucun tableau intermédiaire n'est créé ( source ). Il passe toujours de O (1) à O (n), mais au moins l'itération et le mappage se produisent en une seule itération complète.
Daniel
18

Vous pouvez définir une autre fonction d'itérateur pour boucler sur ceci:

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(','));

Maintenant, vous pourriez demander: pourquoi ne pas simplement utiliser à la Array.fromplace? Étant donné que cela s'exécutera sur tout l'itérateur, enregistrez-le dans un tableau (temporaire), répétez-le, puis effectuez le mappage. Si la liste est énorme (voire potentiellement infinie), cela entraînera une utilisation inutile de la mémoire.

Bien sûr, si la liste des éléments est assez petite, l'utilisation Array.fromdevrait être plus que suffisante.

sheean
la source
Comment une quantité finie de mémoire peut-elle contenir une structure de données infinie?
shinzou
3
ce n'est pas le cas, c'est le point. En utilisant cela, vous pouvez créer des "flux de données" en enchaînant une source d'itérateur à un ensemble de transformations d'itérateur et enfin à un récepteur consommateur. Par exemple, pour le traitement audio en streaming, le travail avec des fichiers énormes, des agrégateurs sur des bases de données, etc.
hraban
1
J'aime cette réponse. Quelqu'un peut-il recommander une bibliothèque qui propose des méthodes de type Array sur les itérables?
Joel Malone
1
mapIterator()ne garantit pas que l'itérateur sous-jacent sera correctement fermé ( iterator.return()appelé) à moins que la valeur de retour suivante n'ait été appelée au moins une fois. Voir: repeater.js.org/docs/safety
Jaka Jančar
11

Cette méthode la plus simple et la plus performante consiste à utiliser le deuxième argument pour Array.fromy parvenir:

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

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

Cette approche fonctionne pour tout itérable non infini . Et cela évite d'avoir à utiliser un appel distinct Array.from(map).map(...)qui itérerait deux fois dans l'itérable et serait pire pour les performances.

Ian Storm Taylor
la source
3

Vous pouvez récupérer un itérateur sur l'itérable, puis renvoyer un autre itérateur qui appelle la fonction de rappel de mappage sur chaque élément itéré.

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
la source
2

Vous pouvez utiliser itiriri qui implémente des méthodes de type tableau pour les itérables:

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
la source
Agréable! C'est ainsi que les API de JS auraient dû être réalisées. Comme toujours, Rust fait bien les choses: doc.rust-lang.org/std/iter/trait.Iterator.html
Flying
1

Jetez un œil à https://www.npmjs.com/package/fluent-iterable

Fonctionne avec tous les itérables (Map, fonction de générateur, tableau) et les itérables asynchrones.

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