Transformando un iterador Javascript en una matriz

171

Estoy tratando de usar el nuevo objeto Map de Javascript EC6, ya que ya es compatible con las últimas versiones de Firefox y Chrome.

Pero lo encuentro muy limitado en la programación "funcional", porque carece de métodos clásicos de mapa, filtro, etc. que funcionen bien con un [key, value]par. Tiene forEach pero eso NO devuelve el resultado de devolución de llamada.

Si pudiera transformarlo map.entries()de un MapIterator en una matriz simple, podría usar el estándar .map, .filtersin hacks adicionales.

¿Hay una "buena" forma de transformar un iterador de Javascript en una matriz? En python es tan fácil como hacerlo list(iterator)... ¡pero Array(m.entries())devuelve una matriz con el iterador como primer elemento!

EDITAR

Olvidé especificar que estoy buscando una respuesta que funcione donde sea que funcione Map, lo que significa al menos Chrome y Firefox (Array.from no funciona en Chrome).

PD.

Sé que existe el fantástico wu.js pero su dependencia de traceur me desanima ...

Stefano
fuente
Consulte también stackoverflow.com/q/27612713/1460043
user1460043

Respuestas:

247

Está buscando la nueva Array.fromfunción que convierte iterables arbitrarios en instancias de matriz:

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

Ahora es compatible con Edge, FF, Chrome y Node 4+ .

Por supuesto, puede valer la pena definir map, filtery métodos similares directamente en la interfaz del iterador, para que pueda evitar la asignación de la matriz. También es posible que desee utilizar una función generadora en lugar de funciones de orden superior:

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;
}
Bergi
fuente
Esperaría que la devolución de llamada reciba (value, key)pares y no (value, index)pares.
Aadit M Shah
3
@AaditMShah: ¿Cuál es la clave de un iterador? Por supuesto, si itera un mapa, podría definiryourTransformation = function([key, value], index) { … }
Bergi
Un iterador no tiene una clave, pero Mapsí tiene pares de valores clave. Por lo tanto, en mi humilde opinión, no tiene ningún sentido definir generales mapy filterfunciones para iteradores. En su lugar, cada objeto iterable debe tener su propia mapy filterfunciones. Esto tiene sentido porque mapy filterson operaciones de preservación de la estructura (quizás no, filterpero mapciertamente lo es) y, por lo tanto, las funciones mapy filterdeben conocer la estructura de los objetos iterables sobre los que están mapeando o filtrando. Piénselo, en Haskell definimos diferentes instancias de Functor. =)
Aadit M Shah
1
@Stefano: Puede calce que fácilmente ...
Bergi
1
@Incognito Ah, sí, claro, eso es cierto, pero es justo lo que pregunta, no es un problema con mi respuesta.
Bergi
45

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

Es superfácil.

De todos modos, los iteradores carecen de reducción, filtro y métodos similares. Debe escribirlos usted mismo, ya que es más eficaz que convertir Map a array y viceversa. Pero no haga saltos Mapa -> Array -> Mapa -> Array -> Mapa -> Array, porque matará el rendimiento.

Ginden
fuente
1
A menos que tenga algo más sustancial, esto realmente debería ser un comentario. Además, Array.fromya ha sido cubierto por @Bergi.
Aadit M Shah
2
Y, como escribí en mi pregunta original, [iterator]no funciona porque en Chrome crea una matriz con un solo iteratorelemento, y [...map.entries()]no es una sintaxis aceptada en Chrome
Stefano
2
El operador de propagación @Stefano ahora acepta la sintaxis en Chrome
Klesun
15

No hay necesidad de transformar un Mapen un Array. Simplemente podría crear mapy filterfunciones para Mapobjetos:

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;
}

Por ejemplo, podría agregar una explosión (es decir, un !carácter) al valor de cada entrada de un mapa cuya clave es primitiva.

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>

También puede agregar mapy filtermétodos Map.prototypepara que se lea mejor. Aunque generalmente no se recomienda modificar los prototipos nativos, creo que se puede hacer una excepción en el caso de mapy filterpara 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>


Editar: En la respuesta de Bergi, creó funciones genéricas mapy filtergeneradoras para todos los objetos iterables. La ventaja de usarlos es que, dado que son funciones generadoras, no asignan objetos iterables intermedios.

Por ejemplo, las funciones my mapy filterdefinidas anteriormente crean nuevos Mapobjetos. Por lo tanto, llamar object.filter(primitive).map(appendBang)crea dos nuevos Mapobjetos:

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

Crear objetos iterables intermedios es costoso. Las funciones del generador de Bergi resuelven este problema. No asignan objetos intermedios, pero permiten que un iterador alimente sus valores perezosamente al siguiente. Este tipo de optimización se conoce como fusión o deforestación en lenguajes de programación funcionales y puede mejorar significativamente el rendimiento del programa.

El único problema que tengo con las funciones generadoras de Bergi es que no son específicas de los Mapobjetos. En cambio, se generalizan para todos los objetos iterables. Por lo tanto, en lugar de llamar a las funciones de devolución de llamada con (value, key)pares (como esperaría al mapear sobre a Map), llama a las funciones de devolución de llamada con (value, index)pares. De lo contrario, es una excelente solución y definitivamente recomendaría usarla sobre las soluciones que proporcioné.

Estas son las funciones específicas del generador que usaría para mapear y filtrar Mapobjetos:

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;
}

Se pueden usar de la siguiente manera:

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 desea una interfaz más fluida, puede hacer algo como esto:

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>

Espero que ayude.

Aadit M Shah
fuente
lo hace gracias! Sin embargo, le di la buena respuesta a @Bergi porque no conocía el "Array.from" y esa es la respuesta más precisa. ¡Muy interesante discusión entre ustedes también!
Stefano
1
@Stefano Edité mi respuesta para mostrar cómo se pueden usar los generadores para transformar correctamente los Mapobjetos usando funciones mapy especializadas filter. La respuesta de Bergi demuestra el uso de genéricos mapy filterfunciones para todos los objetos iterables que no se pueden usar para transformar Mapobjetos porque Mapse pierden las claves del objeto.
Aadit M Shah
Wow, me gusta mucho tu edición. Terminé escribiendo mi propia respuesta aquí: stackoverflow.com/a/28721418/422670 (agregado allí ya que esta pregunta se ha cerrado como un duplicado) porque Array.fromno funciona en Chrome (¡mientras que Map e iteradores sí!). ¡Pero puedo ver que el enfoque es muy similar y podría agregar la función "toArray" a su grupo!
Stefano
1
@Stefano De hecho. Edité mi respuesta para mostrar cómo agregar una toArrayfunción.
Aadit M Shah
7

Una pequeña actualización de 2019:

Ahora, Array.from parece estar disponible universalmente y, además, acepta un segundo argumento mapFn , que le impide crear una matriz intermedia. Esto básicamente se ve así:

Array.from(myMap.entries(), entry => {...});
nromaniv
fuente
Como Array.fromya existe una respuesta con , esto es más adecuado para ser un comentario o una edición solicitada para esa respuesta ... ¡pero gracias!
Stefano
1

Puede usar una biblioteca como https://www.npmjs.com/package/itiriri que implementa métodos de tipo matriz para iterables:

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
}
dimadeveatii
fuente
Esta biblioteca se ve increíble y el conducto que falta para saltar a iterables @dimadeveatii - muchas gracias por escribirlo, lo intentaré pronto :-)
Angelos Pikoulas
0

Puede obtener la matriz de matrices (clave y valor):

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

Y luego, puede obtener valores fácilmente desde adentro, como por ejemplo las claves con el iterador de mapa.

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

También puede usar fluent-iterable para transformar en matriz:

const iterable: Iterable<T> = ...;
const arr: T[] = fluent(iterable).toArray();
kataik
fuente