Transformer un itérateur Javascript en tableau


171

J'essaie d'utiliser le nouvel objet Map de Javascript EC6, car il est déjà pris en charge dans les dernières versions de Firefox et Chrome.

Mais je trouve cela très limité dans la programmation "fonctionnelle", car il manque des méthodes classiques de carte, de filtre, etc. qui fonctionneraient bien avec une [key, value]paire. Il a un forEach mais qui ne renvoie PAS le résultat du rappel.

Si je pouvais transformer son map.entries()MapIterator en un simple tableau, je pourrais alors utiliser le standard .map, .filtersans hacks supplémentaires.

Existe-t-il un "bon" moyen de transformer un itérateur Javascript en un tableau? En python, c'est aussi simple que de le faire list(iterator)... mais Array(m.entries())retournez un tableau avec l'itérateur comme premier élément !!!

ÉDITER

J'ai oublié de préciser que je cherche une réponse qui fonctionne partout où Map fonctionne, ce qui signifie au moins Chrome et Firefox (Array.from ne fonctionne pas dans Chrome).

PS.

Je sais qu'il y a le fantastique wu.js mais sa dépendance au traceur me rebute ...


Réponses:


247

Vous recherchez la nouvelle Array.fromfonction qui convertit les itérables arbitraires en instances de tableau:

var arr = Array.from(map.entries());

Il est désormais pris en charge dans Edge, FF, Chrome et Node 4+ .

Bien sûr, il pourrait être utile de définir map, filteret des méthodes similaires directement sur l'interface iterator, de sorte que vous pouvez éviter l' allocation du tableau. Vous pouvez également utiliser une fonction de générateur au lieu de fonctions d'ordre supérieur:

function* map(iterable) {
    var i = 0;
    for (var item of iterable)
        yield yourTransformation(item, i++);
}
function* filter(iterable) {
    var i = 0;
    for (var item of iterable)
        if (yourPredicate(item, i++))
             yield item;
}

Je m'attendrais à ce que le rappel reçoive des (value, key)paires et non des (value, index)paires.
Aadit M Shah

3
@AaditMShah: Qu'est-ce qu'une clé d'un itérateur? Bien sûr, si vous parcourez une carte, vous pouvez définiryourTransformation = function([key, value], index) { … }
Bergi

Un itérateur n'a pas de clé mais a Mapa des paires clé / valeur. Par conséquent, à mon humble avis, cela n'a aucun sens de définir le général mapet les filterfonctions des itérateurs. Au lieu de cela, chaque objet itérable doit avoir ses propres fonctions mapet filter. Cela a du sens car mapet filtersont des opérations de préservation de la structure (peut-être pas, filtermais c'est mapcertainement le cas) et par conséquent, les fonctions mapet filterdoivent connaître la structure des objets itérables sur lesquels elles mappent ou filtrent. Pensez-y, dans Haskell nous définissons différentes instances de Functor. =)
Aadit M Shah

1
@Stefano: Vous pouvez shim il facilement ...
Bergi

1
@Incognito Ah ok, bien sûr que c'est vrai, mais c'est juste ce que la question demande, pas un problème avec ma réponse.
Bergi

45

[...map.entries()] ou Array.from(map.entries())

C'est super facile.

Quoi qu'il en soit, les itérateurs manquent de méthodes de réduction, de filtrage et similaires. Vous devez les écrire vous-même, car c'est plus efficace que de convertir Map en tableau et inversement. Mais ne faites pas de sauts Map -> Array -> Map -> Array -> Map -> Array, car cela tuerait les performances.


1
À moins que vous n'ayez quelque chose de plus substantiel, cela devrait vraiment être un commentaire. En outre, Array.froma déjà été couvert par @Bergi.
Aadit M Shah

2
Et, comme je l'ai écrit dans ma question initiale, [iterator]ne fonctionne pas car dans Chrome, il crée un tableau avec un seul iteratorélément, et [...map.entries()]n'est pas une syntaxe acceptée dans Chrome
Stefano

2
L' opérateur de diffusion @Stefano est désormais une syntaxe acceptée dans Chrome
Klesun

15

Il n'est pas nécessaire de transformer un Mapfichier Array. Vous pouvez simplement créer mapet filterfonctions pour les Mapobjets:

function map(functor, object, self) {
    var result = new Map;

    object.forEach(function (value, key, object) {
        result.set(key, functor.call(this, value, key, object));
    }, self);

    return result;
}

function filter(predicate, object, self) {
    var result = new Map;

    object.forEach(function (value, key, object) {
        if (predicate.call(this, value, key, object)) result.set(key, value);
    }, self);

    return result;
}

Par exemple, vous pouvez ajouter un bang (c'est-à-dire un !caractère) à la valeur de chaque entrée d'une carte dont la clé est une primitive.

var object = new Map;

object.set("", "empty string");
object.set(0,  "number zero");
object.set(object, "itself");

var result = map(appendBang, filter(primitive, object));

alert(result.get(""));     // empty string!
alert(result.get(0));      // number zero!
alert(result.get(object)); // undefined

function primitive(value, key) {
    return isPrimitive(key);
}

function appendBang(value) {
    return value + "!";
}

function isPrimitive(value) {
    var type = typeof value;
    return value === null ||
        type !== "object" &&
        type !== "function";
}
<script>
function map(functor, object, self) {
    var result = new Map;

    object.forEach(function (value, key, object) {
        result.set(key, functor.call(this, value, key, object));
    }, self || null);

    return result;
}

function filter(predicate, object, self) {
    var result = new Map;

    object.forEach(function (value, key, object) {
        if (predicate.call(this, value, key, object)) result.set(key, value);
    }, self || null);

    return result;
}
</script>

Vous pouvez également ajouter mapet filterméthodes sur Map.prototypepour le faire mieux lire. Bien qu'il ne soit généralement pas conseillé de modifier les prototypes natifs, je pense qu'une exception peut être faite dans le cas mapet filterpour Map.prototype:

var object = new Map;

object.set("", "empty string");
object.set(0,  "number zero");
object.set(object, "itself");

var result = object.filter(primitive).map(appendBang);

alert(result.get(""));     // empty string!
alert(result.get(0));      // number zero!
alert(result.get(object)); // undefined

function primitive(value, key) {
    return isPrimitive(key);
}

function appendBang(value) {
    return value + "!";
}

function isPrimitive(value) {
    var type = typeof value;
    return value === null ||
        type !== "object" &&
        type !== "function";
}
<script>
Map.prototype.map = function (functor, self) {
    var result = new Map;

    this.forEach(function (value, key, object) {
        result.set(key, functor.call(this, value, key, object));
    }, self || null);

    return result;
};

Map.prototype.filter = function (predicate, self) {
    var result = new Map;

    this.forEach(function (value, key, object) {
        if (predicate.call(this, value, key, object)) result.set(key, value);
    }, self || null);

    return result;
};
</script>


Edit: Dans la réponse de Bergi, il a créé des génériques mapetfilter fonctions génératrices pour tous les objets itérables. L'avantage de les utiliser est que, comme ce sont des fonctions génératrices, elles n'allouent pas d'objets itérables intermédiaires.

Par exemple, les fonctions my mapet filterdéfinies ci-dessus créent de nouveaux Mapobjets. Par conséquent, l'appel object.filter(primitive).map(appendBang)crée deux nouveaux Mapobjets:

var intermediate = object.filter(primitive);
var result = intermediate.map(appendBang);

La création d'objets itérables intermédiaires est coûteuse. Les fonctions génératrices de Bergi résolvent ce problème. Ils n'allouent pas d'objets intermédiaires mais permettent à un itérateur de transmettre ses valeurs paresseusement au suivant. Ce type d'optimisation est connu sous le nom de fusion ou de déforestation dans les langages de programmation fonctionnels et peut améliorer considérablement les performances du programme.

Le seul problème que j'ai avec les fonctions génératrices de Bergi est qu'elles ne sont pas spécifiques aux Mapobjets. Au lieu de cela, ils sont généralisés pour tous les objets itérables. Par conséquent, au lieu d'appeler les fonctions de rappel avec des (value, key)paires (comme je m'y attendais lors du mappage sur a Map), il appelle les fonctions de rappel avec(value, index) paires. Sinon, c'est une excellente solution et je recommanderais certainement de l'utiliser sur les solutions que j'ai fournies.

Voici donc les fonctions de générateur spécifiques que j'utiliserais pour mapper et filtrer des Mapobjets:

function * map(functor, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        yield [key, functor.call(that, value, key, entries)];
    }
}

function * filter(predicate, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key    = entry[0];
        var value  = entry[1];

        if (predicate.call(that, value, key, entries)) yield [key, value];
    }
}

function toMap(entries) {
    var result = new Map;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        result.set(key, value);
    }

    return result;
}

function toArray(entries) {
    var array = [];

    for (var entry of entries) {
        array.push(entry[1]);
    }

    return array;
}

Ils peuvent être utilisés comme suit:

var object = new Map;

object.set("", "empty string");
object.set(0,  "number zero");
object.set(object, "itself");

var result = toMap(map(appendBang, filter(primitive, object.entries())));

alert(result.get(""));     // empty string!
alert(result.get(0));      // number zero!
alert(result.get(object)); // undefined

var array  = toArray(map(appendBang, filter(primitive, object.entries())));

alert(JSON.stringify(array, null, 4));

function primitive(value, key) {
    return isPrimitive(key);
}

function appendBang(value) {
    return value + "!";
}

function isPrimitive(value) {
    var type = typeof value;
    return value === null ||
        type !== "object" &&
        type !== "function";
}
<script>
function * map(functor, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        yield [key, functor.call(that, value, key, entries)];
    }
}

function * filter(predicate, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key    = entry[0];
        var value  = entry[1];

        if (predicate.call(that, value, key, entries)) yield [key, value];
    }
}

function toMap(entries) {
    var result = new Map;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        result.set(key, value);
    }

    return result;
}

function toArray(entries) {
    var array = [];

    for (var entry of entries) {
        array.push(entry[1]);
    }

    return array;
}
</script>

Si vous voulez une interface plus fluide, vous pouvez faire quelque chose comme ceci:

var object = new Map;

object.set("", "empty string");
object.set(0,  "number zero");
object.set(object, "itself");

var result = new MapEntries(object).filter(primitive).map(appendBang).toMap();

alert(result.get(""));     // empty string!
alert(result.get(0));      // number zero!
alert(result.get(object)); // undefined

var array  = new MapEntries(object).filter(primitive).map(appendBang).toArray();

alert(JSON.stringify(array, null, 4));

function primitive(value, key) {
    return isPrimitive(key);
}

function appendBang(value) {
    return value + "!";
}

function isPrimitive(value) {
    var type = typeof value;
    return value === null ||
        type !== "object" &&
        type !== "function";
}
<script>
MapEntries.prototype = {
    constructor: MapEntries,
    map: function (functor, self) {
        return new MapEntries(map(functor, this.entries, self), true);
    },
    filter: function (predicate, self) {
        return new MapEntries(filter(predicate, this.entries, self), true);
    },
    toMap: function () {
        return toMap(this.entries);
    },
    toArray: function () {
        return toArray(this.entries);
    }
};

function MapEntries(map, entries) {
    this.entries = entries ? map : map.entries();
}

function * map(functor, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        yield [key, functor.call(that, value, key, entries)];
    }
}

function * filter(predicate, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key    = entry[0];
        var value  = entry[1];

        if (predicate.call(that, value, key, entries)) yield [key, value];
    }
}

function toMap(entries) {
    var result = new Map;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        result.set(key, value);
    }

    return result;
}

function toArray(entries) {
    var array = [];

    for (var entry of entries) {
        array.push(entry[1]);
    }

    return array;
}
</script>

J'espère que cela pourra aider.


il fait merci! Donner la bonne réponse à @Bergi cependant parce que je ne connaissais pas le "Array.from" et c'est la réponse la plus précise. Une discussion très intéressante entre vous aussi!
Stefano

1
@Stefano J'ai édité ma réponse pour montrer comment les générateurs peuvent être utilisés pour transformer correctement des Mapobjets en utilisant des fonctions mapet des filterfonctions spécialisées . La réponse de Bergi démontre l'utilisation de génériques mapet de filterfonctions pour tous les objets itérables qui ne peuvent pas être utilisés pour transformer des Mapobjets car les clés de l' Mapobjet sont perdues.
Aadit M Shah

Wow, j'aime vraiment ton montage. J'ai fini par écrire ma propre réponse ici: stackoverflow.com/a/28721418/422670 (ajouté là-bas car cette question a été fermée en double) car le Array.fromne fonctionne pas dans Chrome (alors que Map et les itérateurs le font!). Mais je peux voir que l'approche est très similaire et que vous pouvez simplement ajouter la fonction "toArray" à votre groupe!
Stefano

1
@Stefano En effet. J'ai modifié ma réponse pour montrer comment ajouter une toArrayfonction.
Aadit M Shah

7

Une petite mise à jour de 2019:

Désormais, Array.from semble être universellement disponible et, de plus, il accepte un deuxième argument mapFn , qui l'empêche de créer un tableau intermédiaire. Cela ressemble essentiellement à ceci:

Array.from(myMap.entries(), entry => {...});

puisqu'une réponse avec Array.fromexiste déjà, c'est plus adapté pour être un commentaire ou une modification demandée à cette réponse ... mais merci!
Stefano

1

Vous pouvez utiliser une bibliothèque comme https://www.npmjs.com/package/itiriri qui implémente des méthodes de type tableau pour les itérables:

import { query } from 'itiriri';

const map = new Map();
map.set(1, 'Alice');
map.set(2, 'Bob');

const result = query(map)
  .filter([k, v] => v.indexOf('A') >= 0)
  .map([k, v] => `k - ${v.toUpperCase()}`);

for (const r of result) {
  console.log(r); // prints: 1 - ALICE
}

Cette bibliothèque a l'air incroyable et le conduit manquant pour accéder aux iterables @dimadeveatii - merci beaucoup de l'avoir écrit, je vais l'essayer bientôt :-)
Angelos Pikoulas

0

Vous pouvez obtenir le tableau de tableaux (clé et valeur):

[...this.state.selected.entries()]
/**
*(2) [Array(2), Array(2)]
*0: (2) [2, true]
*1: (2) [3, true]
*length: 2
*/

Et puis, vous pouvez facilement obtenir des valeurs de l'intérieur, comme par exemple les clés avec l'itérateur de carte.

[...this.state.selected[asd].entries()].map(e=>e[0])
//(2) [2, 3]

0

Vous pouvez également utiliser fluent-iterable pour transformer en tableau:

const iterable: Iterable<T> = ...;
const arr: T[] = fluent(iterable).toArray();
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.