LINQ - Usando Map, Reduce e Filter


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

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 - que precisa atuar o mais próximo possível dos dados;
2- Reduce - que 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 seguire 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.

Assim 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 ($"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 ($"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 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 idéia dos conceitos por trás do map/reduce e como seus equivalantes LINQ operam.

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

"Disse-lhes, pois, Jesus: Quando levantardes o Filho do homem, então conhecereis que EU SOU, e que nada faço por mim mesmo; mas isto falo como meu Pai me ensinou."
João 8:28

Veja os Destaques e novidades do SUPER DVD Visual Basic (sempre atualizado) : clique e confira !

Quer migrar para o VB .NET ?

Quer aprender C# ??

Quer aprender os conceitos da Programação Orientada a objetos ?

Quer aprender o gerar relatórios com o ReportViewer no VS 2013 ?

Referências:


José Carlos Macoratti