Transformando um iterador Javascript em uma matriz

171

Estou tentando usar o novo objeto Map do Javascript EC6, pois ele já é suportado nas versões mais recentes do Firefox e Chrome.

Mas estou achando isso muito limitado na programação "funcional", porque carece de métodos clássicos de mapa, filtro etc. que funcionariam bem com um [key, value]par. Ele tem um forEach, mas que NÃO retorna o resultado do retorno de chamada.

Se eu pudesse transformar seu map.entries()MapIterator em um Array simples, poderia usar o padrão .map, .filtersem hacks adicionais.

Existe uma maneira "boa" de transformar um iterador Javascript em uma matriz? Em python é tão fácil quanto fazer list(iterator)... mas Array(m.entries())retorne uma matriz com o Iterator como seu primeiro elemento !!!

EDITAR

Esqueci de especificar que estou procurando uma resposta que funcione onde quer que o Map funcione, o que significa pelo menos Chrome e Firefox (o Array.from não funciona no Chrome).

PS.

Eu sei que há o fantástico wu.js, mas sua dependência do traceur me deixa desconfortável ...

Stefano
fonte
Veja também stackoverflow.com/q/27612713/1460043
user1460043

Respostas:

247

Você está procurando a nova Array.fromfunção que converte iterables arbitrários em instâncias de array:

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

Agora é suportado no Edge, FF, Chrome e Node 4+ .

Claro, pode valer a pena para definir map, filtere métodos semelhantes diretamente na interface do iterador, para que você possa evitar a alocação da matriz. Você também pode usar uma função de gerador em vez de funções de ordem 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
fonte
Eu esperaria que o retorno de chamada recebesse (value, key)pares e não (value, index)pares.
Aadit M Shah
3
@AaditMShah: O que é a chave de um iterador? Obviamente, se você iterar um mapa, você pode definiryourTransformation = function([key, value], index) { … }
Bergi
Um iterador não possui uma chave, mas a Mappossui pares de valores de chave. Portanto, na minha humilde opinião, não faz sentido definir geral mape filterfunções para iteradores. Em vez disso, cada objeto iterable deve ter seu próprio mape filterfunções. Isso faz sentido porque mape filtersão operações de preservação da estrutura (talvez não, filtermas mapcertamente é) e, portanto, as funções mape filterdevem conhecer a estrutura dos objetos iteráveis ​​que eles estão mapeando ou filtrando. Pense bem, em Haskell definimos diferentes instâncias de Functor. =)
Aadit M Shah
1
@Stefano: Você pode calçar com facilidade ...
Bergi
1
@ Incognito Ah ok, com certeza isso é verdade, mas é exatamente o que a pergunta está pedindo, não é um problema com a minha resposta.
Bergi 22/06
45

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

É super fácil.

Enfim - os iteradores não têm métodos de redução, filtro e similares. Você precisa escrevê-las por conta própria, pois é mais eficiente do que converter o Mapa em array e vice-versa. Mas não faça saltos Mapa -> Matriz -> Mapa -> Matriz -> Mapa -> Matriz, porque isso prejudicará o desempenho.

Ginden
fonte
1
A menos que você tenha algo mais substancial, isso realmente deve ser um comentário. Além disso, Array.fromjá foi coberto pelo @Bergi.
Aadit M Shah
2
E, como escrevi na minha pergunta original, [iterator]não funciona porque no Chrome ele cria um array com um único iteratorelemento nele, e [...map.entries()]não é uma sintaxe aceito no Chrome
Stefano
2
@Stefano operador de propagação é agora aceite sintaxe no Chrome
Klesun
15

Não há necessidade de transformar um Mapem um Array. Você pode simplesmente criar mape filterfunções 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 exemplo, você pode acrescentar um estrondo (ou seja, !caractere) ao valor de cada entrada de um mapa cuja chave seja 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>

Você também pode adicionar mape filtermétodos sobre Map.prototypea fazê-lo ler melhor. Embora geralmente não seja aconselhável modificar protótipos nativos, acredito que uma exceção possa ser feita no caso de mape 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>


Edit: Na resposta de Bergi, ele criou funções genéricas mape filtergeradoras para todos os objetos iteráveis. A vantagem de usá-los é que, como são funções geradoras, eles não alocam objetos iteráveis ​​intermediários.

Por exemplo, minhas funções mape filterdefinidas acima criam novos Mapobjetos. Portanto, a chamada object.filter(primitive).map(appendBang)cria dois novos Mapobjetos:

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

Criar objetos iteráveis ​​intermediários é caro. As funções de gerador da Bergi resolvem esse problema. Eles não alocam objetos intermediários, mas permitem que um iterador alimente seus valores lentamente para o próximo. Esse tipo de otimização é conhecido como fusão ou desmatamento em linguagens de programação funcionais e pode melhorar significativamente o desempenho do programa.

O único problema que tenho com as funções geradoras de Bergi é que elas não são específicas para Mapobjetos. Em vez disso, eles são generalizados para todos os objetos iteráveis. Portanto, em vez de chamar as funções de retorno de chamada com (value, key)pares (como seria de esperar ao mapear sobre a Map), ele chama as funções de retorno de chamada com (value, index)pares. Caso contrário, é uma excelente solução e eu recomendaria definitivamente usá-lo sobre as soluções que forneci.

Portanto, estas são as funções específicas do gerador que eu usaria para mapear e 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;
}

Eles podem ser usados ​​da seguinte maneira:

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>

Se você deseja uma interface mais fluente, pode fazer algo assim:

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 ajude.

Aadit M Shah
fonte
faz obrigado! Porém, dei a boa resposta para @Bergi porque eu não conhecia o "Array.from" e essa é a resposta mais objetiva. Discussão muito interessante entre vocês também!
Stefano
1
@ Stefan Editei minha resposta para mostrar como os geradores podem ser usados ​​para transformar corretamente Mapobjetos usando funções mape filterfunções especializadas . A resposta de Bergi demonstra o uso de funções mape genéricos filterpara todos os objetos iteráveis ​​que não podem ser usados ​​para transformar Mapobjetos porque as chaves do Mapobjeto estão perdidas.
Aadit M Shah
Uau, eu realmente gosto da sua edição. Acabei escrevendo minha própria resposta aqui: stackoverflow.com/a/28721418/422670 (adicionada lá porque essa pergunta foi encerrada como duplicada) porque Array.fromela não funciona no Chrome (enquanto o Map e os iteradores funcionam!). Mas posso ver que a abordagem é muito semelhante e você pode adicionar a função "toArray" ao seu grupo!
Stefano
1
@Stefano Indeed. Editei minha resposta para mostrar como adicionar uma toArrayfunção.
Aadit M Shah
7

Uma pequena atualização de 2019:

Agora Array.from parece estar disponível universalmente e, além disso, aceita um segundo argumento mapFn , que impede a criação de uma matriz intermediária. Isso basicamente se parece com isso:

Array.from(myMap.entries(), entry => {...});
nromaniv
fonte
como Array.fromjá existe uma resposta , isso é mais adequado para ser um comentário ou uma edição solicitada para essa resposta ... mas obrigado!
Stefano
1

Você pode usar uma biblioteca como https://www.npmjs.com/package/itiriri que implementa métodos de 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
fonte
Este lib parece incrível & o conduto faltando para saltar para iterables @dimadeveatii - agradecimentos tanto para escrevê-lo, eu vou experimentá-lo em breve :-)
Angelos Pikoulas
0

Você pode obter a matriz de matrizes (chave e valor):

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

E então, você pode facilmente obter valores de dentro, como por exemplo as chaves com o iterador do mapa.

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

Você também pode usar fluentemente iterável para transformar em matriz:

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