Verwenden von map () auf einem Iterator

86

Angenommen, wir haben eine Karte: let m = new Map();Mit wird m.values()ein Karteniterator zurückgegeben.

Aber ich kann forEach()oder map()auf diesem Iterator nicht verwenden und die Implementierung einer while-Schleife auf diesem Iterator scheint ein Anti-Pattern zu sein, da ES6 Funktionen wie bietet map().

Gibt es also eine Möglichkeit, map()einen Iterator zu verwenden?

Shinzou
quelle
Nicht sofort einsatzbereit, aber Sie können Bibliotheken wie lodash mapFunktionen von Drittanbietern verwenden, die auch Map unterstützen.
gefährlich
Map selbst verfügt über ein forEach zum Durchlaufen seiner Schlüssel-Wert-Paare.
gefährlich
Das Konvertieren des Iterators in ein Array und das Zuordnen Array.from(m.values()).map(...)funktioniert wie folgt, aber ich denke, dies ist nicht der beste Weg, dies zu tun.
JiminP
Welches Problem möchten Sie mit einem Iterator lösen, während ein Array besser für die Verwendung geeignet ist Array#map?
Nina Scholz
1
@NinaScholz Ich verwende ein allgemeines Set wie hier: stackoverflow.com/a/29783624/4279201
Shinzou

Antworten:

80

Der einfachste und am wenigsten performante Weg, dies zu tun, ist:

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

Besser noch

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

Array.fromnimmt jedes iterierbare oder Array-ähnliche Objekt und konvertiert es in ein Array! Wie Daniel in den Kommentaren hervorhebt, können wir der Konvertierung eine Zuordnungsfunktion hinzufügen, um eine Iteration und anschließend ein Zwischenarray zu entfernen.

Mit Array.fromwird Ihre Leistung von O(1)nach O(n)verschoben , wie @hraban in den Kommentaren hervorhebt. Da ma Mapist und sie nicht unendlich sein können, müssen wir uns keine Sorgen um eine unendliche Folge machen. In den meisten Fällen reicht dies aus.

Es gibt noch einige andere Möglichkeiten, eine Karte zu durchlaufen.

Verwenden von forEach

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

Verwenden von 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
quelle
Können Karten unendlich lang sein?
Ktilcu
2
@ktilcu für einen Iterator: ja. Eine .map auf einem Iterator kann als Transformation auf dem Generator betrachtet werden, die einen Iterator selbst zurückgibt. Das Poppen eines Elements ruft den zugrunde liegenden Iterator auf, transformiert das Element und gibt diesen zurück.
Hraban
7
Das Problem bei dieser Antwort ist, dass aus einem O (1) -Speicheralgorithmus ein O (n) -Algorithmus wird, was für größere Datensätze sehr schwerwiegend ist. Abgesehen davon, dass natürlich endliche, nicht streambare Iteratoren erforderlich sind. Der Titel der Frage lautet "Verwenden von map () auf einem Iterator". Ich bin nicht der Meinung, dass faule und unendliche Sequenzen nicht Teil der Frage sind. Genau so verwenden Menschen Iteratoren. Die "Karte" war nur ein Beispiel ("Say .."). Das Gute an dieser Antwort ist ihre Einfachheit, die sehr wichtig ist.
Hraban
1
@hraban Danke, dass du zu dieser Diskussion hinzugefügt hast. Ich kann die Antwort so aktualisieren, dass sie einige Einschränkungen enthält, damit zukünftige Reisende die Info-Front und das Zentrum haben. Wenn es darauf ankommt, müssen wir oft die Entscheidung zwischen einfacher und optimaler Leistung treffen. Ich werde mich normalerweise für eine einfachere (Debuggen, Warten, Erklären) Leistung einsetzen.
Ktilcu
3
@ktilcu Sie können stattdessen aufrufen Array.from(m, ([key,value]) => /* whatever */)(beachten Sie, dass sich die Zuordnungsfunktion innerhalb von befindet from) und dann kein Zwischenarray erstellt wird ( Quelle ). Es bewegt sich immer noch von O (1) nach O (n), aber mindestens Iteration und Mapping erfolgen in nur einer vollständigen Iteration.
Daniel
18

Sie können eine andere Iteratorfunktion definieren, um diese zu durchlaufen:

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

Jetzt könnten Sie fragen: Warum nicht einfach Array.fromstattdessen verwenden? Da dies den gesamten Iterator durchläuft, speichern Sie ihn in einem (temporären) Array, iterieren Sie ihn erneut und führen Sie dann die Zuordnung durch. Wenn die Liste riesig (oder sogar unendlich) ist, führt dies zu einer unnötigen Speichernutzung.

Wenn die Liste der Elemente relativ klein ist, Array.fromsollte die Verwendung natürlich mehr als ausreichend sein.

sheean
quelle
Wie kann eine endliche Speichermenge eine unendliche Datenstruktur enthalten?
Shinzou
3
es tut nicht, das ist der Punkt. Auf diese Weise können Sie "Datenströme" erstellen, indem Sie eine Iteratorquelle mit einer Reihe von Iterator-Transformationen und schließlich einer Consumer-Senke verketten. ZB zum Streamen von Audioverarbeitung, Arbeiten mit riesigen Dateien, Aggregatoren in Datenbanken usw.
hraban
1
Ich mag diese Antwort. Kann jemand eine Bibliothek empfehlen, die Array-ähnliche Methoden für Iterables anbietet?
Joel Malone
1
mapIterator()garantiert nicht, dass der zugrunde liegende Iterator ordnungsgemäß geschlossen ( iterator.return()aufgerufen) wird, es sei denn, der nächste Rückgabewert wurde mindestens einmal aufgerufen. Siehe: repeater.js.org/docs/safety
Jaka Jančar
11

Dieser einfachste und performanteste Weg besteht darin, das zweite Argument zu verwenden Array.from, um dies zu erreichen:

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

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

Dieser Ansatz funktioniert für alle nicht unendlich iterierbaren. Außerdem wird vermieden, dass ein separater Aufruf verwendet werden muss, Array.from(map).map(...)der die Iterable zweimal durchläuft und die Leistung beeinträchtigt.

Ian Storm Taylor
quelle
3

Sie können einen Iterator über die Iterable abrufen und dann einen anderen Iterator zurückgeben, der die Zuordnungsrückruffunktion für jedes iterierte Element aufruft.

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
quelle
2

Sie können itiriri verwenden , das Array-ähnliche Methoden für Iterables implementiert:

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
quelle
Nett! So hätten die APIs von JS funktionieren sollen. Wie immer macht Rust es richtig: doc.rust-lang.org/std/iter/trait.Iterator.html
fliegende Schafe
1

Schauen Sie sich https://www.npmjs.com/package/fluent-iterable an

Funktioniert mit allen iterablen (Map, Generatorfunktion, Array) und asynchronen iterables.

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