Desenvolvimento

2 jul, 2018

LINQ – Usando Map, Reduce e Filter

Publicidade

Neste artigo vou apresentar os conceitos de Map e Reduce, e como usar recursos equivalentes existentes no LINQ com C#.

Já ouviu alguma vez a palavra “MapReduce“?

Vamos iniciar consultando a Wikipedia, que diz:

“MapReduce é um modelo de programação e uma implementação associada para processamento e geração de grandes conjuntos de dados com um algoritmo paralelo e distribuído em um cluster.”

Dessa forma, o padrão Map/Reduce é usado para lidar com grandes quantidades de dados e existem muitos frameworks como o MapReduce e Hadoop que implementam esse padrão. São implementações complexas porque tratam com o problema dos dados distribuídos.

A ideia por trás do padrão MapReduce, é que a maioria das operações envolvendo uma grande quantidade de dados pode ser feita em duas etapas:

  1. Map: precisa atuar o mais próximo possível dos dados;
  2. Reduce: precisa tratar a maior quantidade possível de resultados;

A figura a seguir ilustra um processo Map e Reduce sendo executado em um único computador. Durante este processo, várias threads de trabalho são Mapeadas simultaneamente para várias porções dos dados de entrada, colocando os resultados de mapeamento em uma localização centralizada para posterior processamento por outras threads onde o Reduce opera.

Figura obtida do artigo original: https://blog.jakemdrew.com/2013/01/08/mapreduce-map-reduction-strategies-using-c/

Na LINQ existem implementações que podemos usar para atuar de forma similar com o padrão Map/Reduce.

Abaixo, vemos as implementações LINQ equivalentes a Map, Reduce e Filter:

Map

Select | Enumerable.Range(1, 10).Select(x => x + 2);

Reduce

Aggregate | Enumerable.Range(1, 10).Aggregate(0, (acc, x) => acc + x);

Filter

Where | Enumerable.Range(1, 10).Where(x => x % 2 == 0);

A seguir, veremos situações onde podemos usar os equivalentes LINQ.

1- Map = Select

Vamos supor que temos um conjunto de números inteiros e desejamos elevar todos os números ao quadrado.

Usando a abordagem tradicional, podemos criar um pequeno programa na linguagem C# para realizar tal tarefa:

using System.Collections.Generic;
using System.Linq;
using static System.Console;
namespace CShpMapReduce
{
    class Program
    {
        static List<int> ElevarAoQuadrado(List<int> arr)
        {
            var resultado = new List<int>();
            foreach (var x in arr)
                resultado.Add(x * x);
            return resultado.ToList();
        }
        static void Main(string[] args)
        {
            var numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            var resultado = ElevarAoQuadrado(numeros);
            foreach (var n in resultado)
                Write(n +" ");
            ReadKey();
        }
    }
}

Agora podemos usar o operador Select, que sempre retorna uma coleção IEnumerable que contém elementos baseados em uma função de transformação. É semelhante à cláusula Select, do SQL, que produz um conjunto de resultados.

Veja como ficaria a versão com Select:

using System.Collections.Generic;
using System.Linq;
using static System.Console;
namespace CShpMapReduce
{
    class Program
    {
        static void Main(string[] args)
        {
            var numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            var resultado = numeros.Select(x => x * x).ToArray();
            foreach (var n in resultado)
                Write(n +" ");
            ReadKey();
        }
    }
}

O operador Select itera em cada elemento, e para cada um deles, aplica o método que passamos como um parâmetro via instrução lambda. Isso nos poupa de usar o foreach, e além disso é mais elegante.

Neste caso, passamos o método como uma expressão lambda x => x * x, onde x é cada elemento da matriz passada como um parâmetro da expressão, e x * x é o corpo do método. Assim poderemos transformar os elementos da matriz sem criar nenhum laço.

Dessa forma, o Select retorna uma coleção do mesmo tamanho que a coleção à qual aplicamos a transformação.

2- Reduce = Aggregate

Vamos continuar usando o conjunto de números inteiros do item 1, mas desta vez, queremos somar todos os elementos da coleção.

Na abordagem tradicional podemos usar o código abaixo:

using System.Collections.Generic;
using static System.Console;
namespace CShpMapReduce_2
{
    class Program
    {
        public static int Somar(List<int> arr)
        {
            var acc = 0;
            foreach (var valor in arr)
                acc += valor;
            return acc;
        }
        static void Main(string[] args)
        {
            var numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            var resultado = Somar(numeros);
            WriteLine (quot;Soma = {resultado}");
            ReadKey();
        }
    }
}

Agora vamos usar o operador Aggregate.

Os operadores de agregação executam operações matemáticas, como: Average, Aggregate, Count, Max, Min e Sum, na propriedade numérica dos elementos na coleção.

Ao contrário do operador Select, o operador Aggregate retorna apenas um único valor, que é a combinação dos elementos da coleção à qual aplicamos o método.

using System.Collections.Generic;
using static System.Console;
namespace CShpMapReduce_2
{
    class Program
    {
        static void Main(string[] args)
        {
            var numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            var resultado = numeros.Aggregate((acc, x) => acc + x);
            WriteLine (quot;Soma = {resultado}");
            ReadKey();
        }
    }
}

Nesse código, o acumulador leva o valor inicial de 0, e esse valor é passado como um parâmetro para a expressão lambda, o qual chamamos de acc, sendo que x é cada elemento da coleção, e o corpo de nossa lambda é a soma do acumulador e cada elemento da matriz: acc + x.

3- Filter = Where

Da mesma forma que Select, o operador Where itera sobre a coleção filtrando os elementos de acordo com a função passada.

Assim, o operador Where retorna uma coleção com os elementos filtrados da coleção original que pode ser igual ou menor que a coleção original, podendo inclusive ser uma coleção vazia.

Vejamos um exemplo onde temos uma coleção de strings com nomes de artistas, onde desejamos listar apenas os artistas que contém a letra ‘J’ em seu nome:

using System.Collections.Generic;
using System.Linq;
using static System.Console;
namespace CShpMapReduce3
{
    class Program
    {
        static void Main(string[] args)
        {
            var artistas = new List<string> { "Bob Dylan", "Janis Japlin", "Gregg Allman", "Jim Morrison", "Madonna", "Jimmi Hendrix" };
            var artistasComJ = artistas
                                        .Where(artista => artista.Contains("J"))
                              .ToList();
            foreach (var nome in artistasComJ)
                   Write(nome + " ");
            ReadKey();
        }
    }
}

Da mesma forma que o Select, usando o operador Where apenas passamos a expressão que desejamos aplicar, (artista.Contains(“J”), à coleção para obter o resultado esperado.

Creio que já deu para você der uma ideia dos conceitos por trás do map/reduce, e como seus equivalantes LINQ operam.

Pegue o código do projeto aqui: CShpMapReduce.zip