Uma Suave Introdução ao JavaScript Funcional: Parte 2

Imagem de capa Uma Suave Introdução ao JavaScript Funcional: Parte 2

Essa é a parte 2 de uma série de 4 artigos introduzindo a programação funcional no JavaScript. No artigo anterior, nós vimos como as funções podem ser usadas para fazer abstrações de código de forma mais fácil. Nesse artigo vamos aplicar essas técnicas em listas.

Trabalhando com Arrays e Listas

Relembre que no artigo anterior, falamos sobre código DRY. Vimos que funções são úteis para juntar grupos de ações que podem se repetir. Mas e se estivermos repetindo a mesma função várias vezes? Por exemplo:

function addColour(colour) {
    var rainbowEl = document.getElementById('rainbow');
    var div = document.createElement('div');
    div.style.paddingTop = '10px';
    div.style.backgroundColour = colour;
    rainbowEl.appendChild(div);
}

addColour('red');
addColour('orange');
addColour('yellow');
addColour('green');
addColour('blue');
addColour('purple');

A função addColor é chamada várias vezes. Estamos nos repetindo - algo que queremos evitar. Uma forma de evitar isso é movendo a lista de cores para um array, e chamar addColor em um loop for:

var colours = [
    'red', 'orange', 'yellow',
    'green', 'blue', 'purple'
];

for (var i = 0; i < colours.length; i = i + 1) {
    addColour(colours[i]);
}

Esse código é perfeitamente aceitável. Ele finaliza o trabalho e é menos repetitivo que a versão anterior. Mas não é particularmente expressivo. Temos que dar ao computador instruções muito específicas sobre criar uma variável de indicação (index) e incrementá-la, verificando se é hora de parar. E se pudéssemos encapsular todo esse loop for em uma função?

For-Each (Para Cada)

Uma vez que o JavaScript nos permite passar uma função como parâmetro para outra função, escrever uma função forEach é relativamente simples:

function forEach(callback, array) {
    for (var i = 0; i < array.length; i = i + 1) {
        callback(array[i], i);
    }
}

Essa função recebe outra função, callback, como um parâmetro e a chama em cada item do array.

Agora, com nosso exemplo, nós queremos rodar a função addColor em cada item no array. Usando nossa função forEach podemos expressar essa intenção em apenas uma linha:

forEach(addColour, colours);

Chamar uma função em cada item de um array é uma ferramenta tão útil que implementações modernas do JavaScript incluem isso como uma função nativa nos arrays. Então ao invés de usarmos nossa própria função forEach, podemos usar a função nativa dessa forma:

var colours = [
    'red', 'orange', 'yellow',
    'green', 'blue', 'purple'
];

colours.forEach(addColour);

Você pode achar mais informações sobre o método nativo forEach na referência JavaScript da MDN

Map

Nossa função forEach é útil, mas é de alguma forma limitada. Se a função callback que passamos retornar um valor, forEach apenas ignora-o. Com um pequeno ajuste, podemos alterar nossa função forEach e ela vai nos dar qualquer valor que a função callback retornar. Vamos ter então um novo array com o valor correspondente a cada valor no array original.

Vamos ver um exemplo. Se temos um arrays com IDs, e queremos pegar o elemento correspondente a cada um deles. Em uma solução da forma procedural usaríamos um loop for:

var ids = ['unicorn', 'fairy', 'kitten'];
var elements = [];
for (var i = 0; i < ids.length; i = i + 1) {
    elements[i] = document.getElementById(ids[i]);
}
// elements now contains the elements we are after

Novamente, tivemos que dizer ao computador como criar uma variável index e incrementá-la - detalhes que realmente não devemos nos preocupar. Vamos refatorar o loop for assim como fizemos com forEach e colocá-lo numa função chamada map:

var map = function(callback, array) {
    var newArray = [];
    for (var i = 0; i < array.length; i = i + 1) {
        newArray[i] = callback(array[i], i);
    }
    return newArray;
}

A função map pega funções pequenas e triviais, tornando-as em funções super-herói - ela multiplica a efetividade da função aplicando-a em um array inteiro apenas com uma chamada.

Assim como forEach, map é tão útil que implementações modernas do JavaScript a tem como um método nativo para objetos array. Você pode chamar o método nativo da seguinte forma:

var ids = ['unicorn', 'fairy', 'kitten'];
var getElement = function(id) {
  return document.getElementById(id);
};
var elements = ids.map(getElement);

Você pode ler mais sobre o método nativo map na referência JavaScript da MDN.

Reduce

map é muito útil, mas nós podemos fazer uma função ainda mais poderosa se pegarmos um array inteiro e retornar apenas um valor. Isso pode parecer inicialmente um pouco contraintuitivo - como uma função que retorna um valor ao invés de vários é mais poderosa? Para descobrir o porquê, temos que primeiro olhar como essa função trabalha.

Para ilustrar, vamos considerar dois problemas similares:

  1. Dado um array de números, calcule a soma; e
  2. Dado um array de palavras, junte-as com um espaço entre cada palavra.

Isso pode parecer exemplos bobos, triviais - e eles são. Mas tenha paciência comigo, após vermos como a função reduce trabalha, nós vamos aplicá-la de formas mais interessantes.

A forma "procedural" de resolver esses problemas é, novamente, com loops for:

// Given an array of numbers, calculate the sum
var numbers = [1, 3, 5, 7, 9];
var total = 0;
for (var i = 0; i < numbers.length; i = i + 1) {
    total = total + numbers[i];
}
// total is 25

// Given an array of words, join them together with a space between each word.
var words = ['sparkle', 'fairies', 'are', 'amazing'];
var sentence = '';
for (i = 0; i < words.length; i++) {
    sentence = sentence + ' ' + words[i];
}
// ' sparkle fairies are amazing'

Essas duas soluções tem muito em comum. Ambas usam um loop for para iterar sobre um array; ambas tem uma variável trabalhando ( total e sentence); e ambas definem o valor trabalhado com um valor inicial.

Vamos refatorar a parte interior de cada loop, e tornar isso em uma função:

var add = function(a, b) {
    return a + b;
}

// Given an array of numbers, calculate the sum
var numbers = [1, 3, 5, 7, 9];
var total = 0;
for (i = 0; i < numbers.length; i = i + 1) {
    total = add(total, numbers[i]);
}
// total is 25

function joinWord(sentence, word) {
    return sentence + ' ' + word;
}

// Given an array of words, join them together with a space between each word.
var words = ['sparkle', 'fairies', 'are', 'amazing'];
var sentence = '';
for (i = 0; i < words.length; i++) {
    sentence = joinWord(sentence, words[i]);
}
// 'sparkle fairies are amazing'

Isso é muito mais conciso e o padrão ficou claro. Ambas funções interiores pegam a variável de trabalho como primeiro parâmetro e o elemento atual do array como segundo. Agora que conseguimos ver o padrão de forma mais clara, nós podemos mover os loops for para dentro da função:

var reduce = function(callback, initialValue, array) {
    var working = initialValue;
    for (var i = 0; i < array.length; i = i + 1) {
        working = callback(working, array[i]);
    }
    return working;
};

Agora que temos nossa função reduce, vamos usá-la:

var total = reduce(add, 0, numbers);
var sentence = reduce(joinWord, '', words);

Assim como forEach e map, reduce também é nativa no padrão JavaScript como um método do objeto Array. Podemos usá-la assim:

var total = numbers.reduce(add, 0);
var sentence = words.reduce(joinWord, '');

Você pode ler mais sobre o método nativo reduce na referência JavaScript do MDN.

Juntando Tudo

Como mencionamos anteriormente, esses são exemplos triviais - as funções add e joinWord são muito simples - e esse é o ponto na verdade. Funções simples e pequenas são mais fáceis de pensar e testar. Mesmo quando pegamos duas funções pequenas e simples e as combinamos (como add e reduce por exemplo), o resultado ainda é mais fácil de se imaginar do que uma única função gigante e complicada. Mas, com isso dito, podemos fazer coisas mais interessantes do que adicionar números.

Vamos tentar fazer algo um pouco mais complicado. Começaremos com dados formatados de forma não convencional, e usar as funções map e reduce para transformar isso em uma lista HTML. Aqui estão nossos dados:

var ponies = [
    [
        ['name', 'Fluttershy'],
        ['image', 'https://tinyurl.com/gpbnlf6'],
        ['description', 'Fluttershy is a female Pegasus pony and one of the main characters of My Little Pony Friendship is Magic.']
    ],
    [
        ['name', 'Applejack'],
        ['image', 'https://tinyurl.com/gkur8a6'],
        ['description', 'Applejack is a female Earth pony and one of the main characters of My Little Pony Friendship is Magic.']
    ],
    [
        ['name', 'Twilight Sparkle'],
        ['image', 'https://tinyurl.com/hj877vs'],
        ['description', 'Twilight Sparkle is the primary main character of My Little Pony Friendship is Magic.']
    ]
];

Os dados não estão muito arrumados. Seria muito mais claro se os arrays internos fossem objetos bem formatados. Previamente, nós usamos a função reduce para calcular simples valores como strings e números, mas ninguém disse que o valor retornado por reduce tem que ser simples. Nós podemos usar essa função com objetos, arrays ou até mesmo elementos DOM. Vamos criar uma função que recebe um desses arrays internos (como ['name', 'Fluttershy']) e adiciona esse par chave/valor a um objeto.

var addToObject = function(obj, arr) {
    obj[arr[0]] = arr[1];
    return obj;
};

Com essa função addToObject podemos converter cada "pequeno" array em um objeto:

var ponyArrayToObject = function(ponyArray) {
    return reduce(addToObject, {}, ponyArray);
};

Se usarmos nossa função map poderemos converter todo o array em algo mais arrumado:

var tidyPonies = map(ponyArrayToObject, ponies);

Agora temos um array de pequenos objetos. Com uma ajuda do pequeno template engine de Thomas Fuchs, podemos usar reduce novamente para converter isso em um fragmento HTML. A função template pega uma string template e um objeto, e onde ela achar palavras envoltas com chaves (como {name} ou {image}), ela vai trocá-las com o valor correspondente no objeto. Por exemplo:

var data = { name: "Fluttershy" };
t("Hello {name}!", data);
// "Hello Fluttershy!"

data = { who: "Fluttershy", time: Date.now() };
t("Hello {name}! It's {time} ms since epoch.", data);
// "Hello Fluttershy! It's 1454135887369 ms since epoch."

Então se quisermos converter um pequeno objeto em um item de uma lista, podemos fazer algo assim:

var ponyToListItem = function(pony) {
    var template = '<li><img src="{image}" alt="{name}"/>' +
                   '<div><h3>{name}</h3><p>{description}</p>' +
                   '</div></li>';
    return t(template, pony);
};

Isso nos dá uma forma de converter um item individual em HTML, mas para converter todo o array, precisaremos das nossas funções reduce e joinWords:

var ponyList = map(ponyToListItem, tidyPonies);
var html = '<ul>' + reduce(joinWord, '', ponyList) + '</ul>';

Você pode ver o resultado final aqui

Uma vez que você entenda os padrões onde map e reduce são adequados, você não vai mais precisar de escrever um loop for da maneira antiga. De fato, é um desafio útil ver se você consegue evitar completamente a escrita de loops for no seu próximo projeto. Depois que você usar map e reduce algumas vezes, você vai começar a notar ainda mais padrões que podem ser abstraídos. Alguns comuns incluem filtrar e arrancar valores de um array. Uma vez que esses padrões aparecem regularmente, algumas pessoas criaram bibliotecas de programação funcional, assim você pode reutilizar código para solucionar padrões comuns. Algumas das bibliotecas mais populares são:

Agora que você viu quão útil é passar funções como variáveis, especialmente quando estamos lidando com listas, você deve ter um grande novo conjunto de técnicas no seu cinto de utildades metafórico. E se você escolher ir embora agora, está tudo bem. Você pode finalizar a leitura e ninguém vai pensar nada de ruim sobre você. Você pode ir e ser um programador bem sucedido e produtivo, e nunca perturbar seus sonhos com as complexidades da partial application (aplicação parcial), currying e composition (composição). Essas coisas não são para todo mundo.

Artigo Original: A Gentle introduction to functional Javascript: PART 2