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?


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

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.


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.


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

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

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.