Dia desses, o Matheus Lima reuniu uma galera na Concrete Solutions, em São Paulo, para realizar um workshop de Programação Funcional. A linguagem usada foi JavaScript, mas o paradigma pode ser usado para qualquer linguagem. Se você não pôde vir, aqui está o vídeo com a transmissão ao vivo:
As soluções para o Workshop já estão no GitHub do Matheus. E abaixo ele resume tudo em um artigo. Preparado? Vamos lá!
Você já percebeu que cada vez mais o termo Programação Funcional vem sendo usado pela comunidade? No meu último artigo “O que TODO desenvolvedor JavaScript precisa saber“, um dos pontos que gerou mais dúvida foi justamente o da Programação Funcional.
Continue lendo esse artigo para aprender:
- Quais as vantagens de usar a Programação Funcional
- Como usá-la tanto em ES5 quanto em ES6
- O que são Pure Functions e Higher-Order Functions
- Qual a diferença entre Map, Filter e Reduce
- O que é Currying
- Como compor funções de maneira eficaz
Para entender as verdadeiras motivações, temos obrigatoriamente que voltar aos conceitos básicos.
A função abaixo possui inputs e outputs bem definidos:
function square(x) { return x * x; } square(2); // 4
Ela recebe como parâmetro uma variável x e retorna um int que é a multiplicação de x com ele mesmo.
A função abaixo, porém, não possui inputs e outputs tão bem definidos:
function generateDate() { var date = new Date(); generate(date); } generateDate(); // ???
Ela não recebe nada como parâmetro e retorna o que parece ser uma data processada, mas não temos como ter certeza.
Um ponto que vale ser reforçado: só porque não declaramos explicitamente os inputs e outputs dessa função, não quer dizer que ela não os tenha. Eles apenas estão ocultos. E isso pode gerar um dos piores problemas nas aplicações modernas: os Efeitos Colaterais (side-effects).
Funções como as de cima que possuem inputs e outputs ocultos e podem gerar side-effects são chamadas de Funções Impuras (impure functions). Outra característica importante que elas têm é que se invocarmos uma função impura diversas vezes, o retorno nem sempre será o mesmo. O que dificulta a manutenção e os testes na sua aplicação.
Funções Puras (pure functions), por outro lado, como o primeiro exemplo desse artigo, têm inputs e outputs declarados e não geram side-effects. Além disso, o retorno de uma função pura dado um parâmetro será sempre o mesmo. Obviamente os seus testes serão mais fáceis de desenvolver, assim como a manutenção da sua aplicação.
E por que estamos falando sobre isso? Porque escrever funções puras e remover side-effects é a base da Programação Funcional.
1. Higher-Order Functions
Matematicamente falando, funções que operam sobre outras funções (as recebendo como parâmetro ou as retornando) são chamadas de Higher-Order Functions.
Essas funções nos permitem fazer abstrações não apenas de valores, mas também de ações, como no exemplo abaixo:
var calculate = function(fn, x, y) { return fn(x, y); };
A função calculate recebe três parâmetros. O primeiro é uma função qualquer que será invocada passando como parâmetro x e y.
Pensando em um cenário em que precisamos tanto de uma soma quanto de uma multiplicação, podemos pensar na solução dessa forma em ES5:
var sum = function(x, y) { return x + y; }; var mult = function(x, y) { return x * y; }; calculate(sum, 2, 5); // 7 calculate(mult, 2, 5); // 10
Ou dessa, bem mais curta, em ES6:
const sum = (x, y) => x + y; const mult = (x, y) => x * y; calculate(sum, 2, 5); // 7 calculate(mult, 2, 5); // 10
Higher-order functions estão em todos os lugares no ecossistema do JavaScript. Se você já usou testes unitários com Jasmine ou Mocha, então o trecho abaixo deve ser familiar:
describe("A suite", function() { it("contains spec with an expectation", function() { expect(true).toBe(true); }); });
Percebemos que o segundo parâmetro tanto de describe quanto de it são funções. Por definição, ambas são higher-order functions.
Outro exemplo também é o bom e velho jQuery. Podemos perceber que praticamente todo o código gerado por ele era composto de higher-order functions, como o exemplo abaixo:
$(btn1).click(function() { doSomething(); }); $(btn2).bind("click", function() { doSomethingElse(); }); $(document).ajaxStart(function() { $(log).text("Triggered ajaxStart handler."); });
Em aplicações desenvolvidas com AngularJS também não é diferente, observe atentamente a definição de um controller:
var app = angular.module('app'); app.controller('MyController', function() { /* */ });
Agora que já temos conhecimento dos fundamentos (pure functions e higher-order functions), podemos nos aprofundar um pouco mais…
2. Map
A função map invoca um callback e retorna um novo array com o resultado desse callback aplicado em cada item do array inicial.
Imaginando um cenário em que temos um array de inteiros e precisamos do quadrado de cada valor desse array, podemos fazer dessa forma bem simples, usando a função map em ES5:
var numbers = [1, 2, 3]; var square = function(x) { return x * x; }; var squaredNumbers = numbers.map(square); // [1, 4, 9]
Ou assim em ES6:
const numbers = [1, 2, 3]; const square = x => x * x; const squaredNumbers = numbers.map(square); // [1, 4, 9]
Nesse outro cenário abaixo, percebemos o reaproveitamento de código que podemos conseguir ao usar o map.
Possuímos dois arrays de objetos diferentes, porém ambos têm o campo name, e precisamos de uma função que retorne um novo array apenas com os names dos objetos:
var students = [ { name: 'Anna', grade: 6 }, { name: 'John', grade: 4 }, { name: 'Maria', grade: 9 } ]; var teachers = [ { name: 'Mark', salary: 2500 }, { name: 'Todd', salary: 3700 }, { name: 'Angela', salary: 1900 } ]; var byName = function(object) { return object.name; }; var byNames = function(list) { return list.map(byName); }; byNames(students); // ["Anna", "John", "Maria"] byNames(teachers); // ["Mark", "Todd", "Angela"]
Em ES6:
const students = [ { name: 'Anna', grade: 6 }, { name: 'John', grade: 4 }, { name: 'Maria', grade: 9 } ]; const teachers = [ { name: 'Mark', salary: 2500 }, { name: 'Todd', salary: 3700 }, { name: 'Angela', salary: 1900 } ]; const byName = object => object.name; const byNames = list => list.map(byName); byNames(students); // ["Anna", "John", "Maria"] byNames(teachers); // ["Mark", "Todd", "Angela"]
Podemos melhorar ainda mais esse trecho de código alterando as funções byName e byNames para que o atributo name não esteja mais tão acoplado. Podemos simplesmente receber como parâmetro qualquer atributo e aplicá-lo à função. Fica como exercício.
3. Filter
A função filter é bem semelhante ao map: ela também recebe um callback como parâmetro e também retorna um novo array. A única diferença é que filter, como o próprio nome diz, retorna um filtro dos elementos do array inicial baseado na função de callback.
Imaginemos que temos um array de inteiros e desejamos retornar apenas aqueles que são maiores do que 4. Podemos resolver assim usando o filter com ES5:
var numbers = [1, 4, 7, 10]; var isBiggerThanFour = function(value) { return value > 4; }; var numbersBiggerThanFour = numbers.filter(isBiggerThanFour); // [7, 10]
Ou com ES6:
const numbers = [1, 4, 7, 10]; const isBiggerThanFour = value => value > 4; const numbersBiggerThanFour = numbers.filter(isBiggerThanFour); // [7, 10]
Outro exercício é uma melhoria na função isBiggerThanFour. Deveríamos alterá-la para receber como parâmetro qualquer inteiro que desejamos fazer a comparação.
4. Reduce
Uma das funções que mais gera dúvidas é o reduce. Ele recebe como parâmetro um callback e um valor inicial, com o objetivo de reduzir o array a um único valor. O cenário mais comum para explicar o reduce é uma soma:
var numbers = [1, 2, 3]; var sum = function(x, y) { return x + y; }; var numbersSum = numbers.reduce(sum, 0); // 6
Com ES6 seria assim:
const numbers = [1, 2, 3]; const sum = (x, y) => x + y; const numbersSum = numbers.reduce(sum, 0); // 6
O primeiro parâmetro é a função que será aplicada; no caso, uma soma. E o segundo parâmetro é o valor inicial. Se por algum motivo precisássemos começar a soma com 10, faríamos dessa forma:
const numbers = [1, 2, 3]; const sum = (x, y) => x + y; const numbersSum = numbers.reduce(sum, 10); // 16
Mas o reduce não serve apenas para somas, podemos também trabalhar com strings. Imaginando que nós temos um array de meses e precisamos retornar o meses dessa forma: JAN/FEV/MAR … / DEZ.
Podemos fazer assim:
var months = ['JAN', 'FEV', 'MAR', /*...*/ , 'DEZ']; var monthsShortener = function(previous, current) { return previous + '/' + current; }; var shortenedMonths = months.reduce(monthsShortener, ''); // /JAN/FEV/MAR ... /DEZ
Não era bem o que a gente queria inicialmente. Queríamos isso: JAN/FEV/MAR … / DEZ
Mas obtivemos isso: /JAN/FEV/MAR … / DEZ
Devemos alterar nossa função monthsShortener para adicionar uma condição que faça a prevenção desse erro:
var months = ['JAN', 'FEV', 'MAR', /*...*/ , 'DEZ']; var monthsShortener = function(previous, current) { if (previous === '') { return current; } else { return previous + '/' + current; } }; var shortenedMonths = months.reduce(monthsShortener, ''); // JAN/FEV/MAR ... /DEZ
Feito! E também na versão em ES6:
const months = ['JAN', 'FEV', 'MAR', /*...*/ , 'DEZ']; const monthsShortener = (previous, current) => { if (previous === '') { return current; } else { return previous + '/' + current; } }; const shortenedMonths = months.reduce(monthsShortener, ''); // JAN/FEV/MAR ... /DEZ
5. Currying
A técnica de transformar uma função com múltiplos parâmetros em uma sequência de funções que aceitam apenas um parâmetro é chamada de Currying.
Se na teoria ficou confuso, na prática seria transformar isso:
var add = function(x, y) { return x + y; }; add(1, 2) // 3
Nisso:
var add = function(x) { return function(y) { return x + y; }; };
A princípio, parece que estamos apenas adicionando mais dificuldade sem nenhum ganho. Porém, temos uma grande vantagem: transformar 0 código em pequenos pedaços mais expressivos e com maior reuso.
Pensando em uma aplicação que possui diversos trechos do código uma soma com 5 e outra com 10, podemos usar a segunda versão da função add dessa forma:
var addFive = add(5); var addTen = add(10); addFive(3); // 8 addFive(1); // 6 addTen(1); // 1 addTen(10); //20
Mais um exemplo seria um Hello World simples com uma curried function. Podemos implementá-lo desse jeito com ES5:
var greeting = function(greet) { return function(name) { return greet + ' ' + name; }; }; var hello = greeting('Hello'); hello('World'); // Hello World hello('Matheus'); // Hello Matheus
Ou com ES6:
const greeting = greet => name => greet + ' ' + name; const hello = greeting('Hello'); hello('World'); // Hello World hello('Matheus'); // Hello Matheus
6. Compose
Podemos compor funções pequenas para gerar outras mais complexas de forma bem fácil em JavaScript. A vantagem é o poder de usar essas funções mais complexas, de forma simples, em toda aplicação. Ou seja, aumentamos o reuso.
Por exemplo, em uma aplicação em que necessitamos de uma função para transformar uma string passada pelo usuário em um grito: mudar para caracteres maiúsculos e adicionar uma exclamação no final. Podemos fazer assim em ES5:
var compose = function(f, g) { return function(x) { return f(g(x)); }; }; var toUpperCase = function(x) { return x.toUpperCase(); }; var exclaim = function(x) { return x + '!'; }; var angry = compose(toUpperCase, exclaim); angry('ahhh'); // AHHH!
Ou em ES6:
const compose = (f, g) => x => f(g(x)); const toUpperCase = x => x.toUpperCase(); const exclaim = x => x + '!'; const angry = compose(toUpperCase, exclaim); angry('ahhh'); // AHHH!
Ficou alguma dúvida ou tem algum comentário? Fique à vontade nos comentários.