Programação funcional? Quando eu ouvi esse termo pela primeira vez, logo pensei: Mais uma heurística computacional que logo será esquecida e novamente sobreposta pela orientação a objetos, mas quando fui estudar a fundo o que era, notei que não faz muito sentido comparar esses paradigmas, ambos podem conviver muito bem juntos em um projeto.
Abaixo, um índice do que será apresentado nesse post:
1 – Conceitos básicos
2 – Currying
3 – Hight order functions
4 – Filter, Map, Reduce, Some, Every
5 – Compose
Conceitos básicos
No início desse post eu disse que não faz sentido comparar programação funcional com orientação a objetos, isso porque a programação funcional é basicamente a maneira como uma determinada solução é escrita. Suas funções são normalmente expressas por meio de outras funções, sendo assim, para obter o valor da função enviando para mesma um conjunto de parâmetros, envolve não só aplicar as regras daquela função inicial, mas também fazer uso de outras funções para obter um resultado. Confuso? Não se preocupe, é mais simples do que parece e ao decorrer desse post iremos aprender algumas coisas interessantes.
Antes de iniciarmos de fato, precisamos voltar um pouco a alguns conceitos mais básicos. Vamos considerar o seguinte código:
let square = (x) => x * x; square(2); // 4
A função apresentada anteriormente tem seu input e output muito bem definidos e explícitos. Ela espera receber um int, que será multiplicado por ele mesmo e retornado ao final da execução da função. Esse tipo de função é chamada de função pura.
Porém, nem sempre nossas funções tem inputs e outputs tão bem definidos, a função abaixo ilustra esse comportamento:
let date = () => let date = new Date(); generate(date); date(); // ???
Essa função retorna uma data processada, mas não temos como saber exatamente qual o seu comportamento e qual valor esperar, isso dificulta a manutenção, testabilidade, e reaproveitamento de código. Até porque, se a invocarmos mais de uma vez, seu retorno não necessariamente será sempre o mesmo.
Lembre-se, não é porque não definimos explicitamente um input e output para a função que significa que ela não os tenha. Esse tipo de função é chamada de função impura.
Esse comportamento inesperado da função gera o que chamamos de efeitos colaterais (side effects). Veja o link para entender rapidamente o que são os efeitos colaterais.
Legal, mas o que tudo isso tem haver com programação funcional? Bom, uma de suas premissas também é evitar e remover as funções impuras e reduzir os side effects.
Currying
Para que os conceitos e funcionamentos por trás do Currying não fiquem confusos, você precisa entender primeiramente como funciona o escopo do JavaScript, como funcionam as Closures. Se não está seguro com esse conceito, recomendo que leia esse pequeno artigo antes de continuar a leitura.
Currying nada mais é do que a transformação de uma função que receberia mais de um parâmetro em uma série de chamadas a outras funções. Dessa maneira você garante que cada função resolva uma ação especifica e o código se torna muito mais previsível, e de fácil reutilização.
Acompanhe o exemplo abaixo.
let getName = (firstName, lastName) => firstName + " " + lastName; getName("Walker", "de Paula"); //"Walker de Paula"
A função acima espera um nome e sobrenome obrigatoriamente como parâmetros, mas poderíamos reescrever facilmente esta função usando Currying realizando um aninhamento simples, de modo que a função básica requer apenas o primeiro nome, e então retorna outra função que espera receber o sobrenome. Veja abaixo.
let setName = (firstName) => (lastName) => firstName + " " + lastName setName('Walker')('de Paula') // Walker de Paula
O legal é que você não é obrigado a passar todos os parâmetros de uma só vez caso ainda não os tenha. Você pode chamar a primeira função passando o primeiro parâmetro, ela retornará outra função e assim sucessivamente até retornar o valor no ultimo aninhamento.Veja o exemplo abaixo com mais níveis de aninhamento.
let greet = (greeting) => (emphasis) => (separator) => (firstName) => (lastName) => greeting + separator + firstName + lastName + emphasis; let greetUsers = greet("Olá")(', ')("."); greetUsers("Walker ")('de Paula'); //"Olá, Walker de Paula."
Ainda podemos definir novos parâmetros caso precisemos, veja abaixo:
greetUsers(" sr. ")("Walker ")('de Paula'); //"Olá sr. Walker de Paula."
Currying é uma técnica muito simples e muito interessante que pode ser facilmente manipulada, e expressa exatamente o que eu disse na explicação da teoria do paradigma da programação funcional, no início desse post.
Hight order functions
Não se intimide pelo nome, já utilizamos isso no JavaScript desde que aprendemos a fazer nossas primeiras funções de click. Hight order function nada mais é do que uma função que aceita como parâmetros outra função e a processa antes de retornar seu valor, que por sua vez, também pode ser outra função. Calma, eu sei que é confuso, mas é mais simples do que parece. Veja abaixo alguns exemplos bastante simples.
let calculateBy = (callback, x, y) => callback(x, y);
A função acima é bastante simples e espera 3 parâmetros, sendo o primeiro uma função e o segundo e terceiro um int. Vamos imaginar que hora precisamos somar e hora precisamos multiplicar os valores que passaremos a essa função. Nesse cenário, poderíamos declarar o seguinte:
let sum = (x, y) => x + y; let multiplication = (x, y) => x * y; calculateBy(sum, 10, 10); // 20 calculateBy(multiplication, 10, 10); // 100
Até agora vimos quais são os fundamentos da programação funcional e temos conhecimento de algumas coisas que antes pareciam confusas. Vimos também que esses elementos estão presente em todo ciclo de vida de uma aplicação JavaScript e independem da utilização da orientação a objetos, eventos ou qualquer que seja o paradigma.
Filter, Map, Reduce, Some, Every
Na hora de iniciar um novo projeto, muitos programadores recorrem rapidamente para o UnderscoreJS ou Lodash, que são ótimas bibliotecas para trabalhar com Arrays, Objetcs e Collections, porém, para alguns projetos mais simples e menores você acaba não utilizando nem 10% do que essas bibliotecas possuem e seus problemas poderiam ter sido resolvido apenas utilizando alguns recursos nativos que estão presentes na cadeia de protótipos dos Arrays do JavaScript, por exemplo.
Falaremos agora de alguns métodos bastante úteis que estão presentes no protótipo dos Arrays que vão salvar sua pele em muitas ocasiões e fazem todo sentido com tudo que foi visto até agora.
Filter
O método Filter como o próprio nome já diz, filtra os elementos de um Array para retornar um novo Array com os elementos que atendem as condições impostas em sua função de callback.
É importante dizer também que o Filter não altera a Array a partir da qual foi invocado. Veja o exemplo abaixo para entender melhor seu funcionamento.
Vamos supor que precisamos pegar os valores dentro de uma lista de inteiros que sejam iguais ou superiores a 10.
let isBigEnough = (item) => item >= 10; let itens = [12, 5, 8, 130, 44]; itens.filter(isBigEnough); // [12, 130, 44]. itens ainda é [12, 5, 8, 130, 44]
Map
O Map funciona de maneira semelhante ao Filter, é invocada a partir de um Array e recebe um callback que é passado por argumento para cada elemento do Array. Seu retorno também é um novo Array que não altera o valor do Array a partir do qual foi chamado.
Para este exemplo, vamos imaginar que precisamos multiplicar todos os inteiros de uma lista.
let numbers = [1, 4, 9]; let doubles = numbers.map( (number) => return number * 2; ); // doubles é [2, 8, 18]. numbers ainda é [1, 4, 9]
Podemos utilizar o Map também para inserir algum parâmetro dentro de um objeto que está em um Array.
let users = [ { id: 15 }, { id: -1 }, { id: 0 }, { id: 3 }, { id: 12.2 } ]; let newUsers = users.map( (user) => user.name = “Novo Nome” return user; ); // newUsers é [ { id: 15, name: “Novo Nome” }, { id: -1, name: “Novo Nome” }, { id: 0, name: “Novo Nome” }, { id: 3, name: “Novo Nome” }, { id: 12.2, name: “Novo Nome” }] // users ainda é [ { id: 15 }, { id: -1 }, { id: 0 }, { id: 3 }, { id: 12.2 }]
Reduce
Reduce recebe um callback e um valor inicial como parâmetro, e também é invocado a partir de um Array, seu objetivo é reduzir o Array que lhe é passado a um único valor que pode ser outro Array, int, String.
No cenário abaixo estamos somando os inteiros de um Array.
let numbers = [1, 2, 3]; let sum = (valorSomado, itemArray) => { valorSomado += itemArray; return valorSomado; }; let numbersSum = numbers.reduce(sum, 0); // 6
Some
O método Some funciona exatamente igual ao método Filter, a única diferença é que seu retorno será um Boolean e não um novo Array.
Como o próprio nome já diz, o método Some irá verificar em um Array se algum de seus elementos atendem aos testes que são impostos em sua função de callback.
No exemplo abaixo, temos um Array de objetos e cada objeto representa um input checkbox. Precisamos verificar se algum desses Inputs está checado ou não.
let inputs = [ {name: “Input 1”, isChecked: false}, {name: “Input 2”, isChecked: false}, {name: “Input 3”, isChecked: false}, ]; inputs.some( (input) => return input.isChecked === true ); // false
porém se alterarmos o valor de um input para isChecked: true, o resultado será:
let inputs = [ {name: “Input 1”, isChecked: false}, {name: “Input 2”, isChecked: true}, {name: “Input 3”, isChecked: false}, ]; inputs.some( (input) => return input.isChecked === true ); // true
Every
O método Every é bem parecido com o Some. Vamos utilizar o mesmo exemplo anterior para demonstrar seu comportamento.
let inputs = [ {name: “Input 1”, isChecked: false}, {name: “Input 2”, isChecked: true}, {name: “Input 3”, isChecked: false}, ]; inputs.some( (input) => return input.isChecked === true ); // true inputs.every( (input) => return input.isChecked === true ); // false
Como podemos observar acima, o método Every está verificando se todos os elementos do Array estão coniventes com a condição imposta pelo seu callback, diferentemente do Some, que verifica se pelo menos um item do Array passa na condição imposta pelo callback. Logo, se alterarmos todos os valores de todos inputs para isChecked: true o resultado deverá resultar em true, como mostra o exemplo abaixo.
let inputs = [ {name: “Input 1”, isChecked: true}, {name: “Input 2”, isChecked: true}, {name: “Input 3”, isChecked: true}, ]; inputs.every( (input) => return input.isChecked === true ); // true
Compose
Essa interessante técnica consiste em construir funções complexas a partir de funções mais simples, fragmentando-as utilizando Currying.
Para ilustrar esse comportamente, vamos supor que precisamos formatar uma String específica. Veja o exemplo abaixo.
const COMPOSE = (f, g) => x => f(g(x)); let toLowerCase = (x) => x.toLowerCase(); let exclaim = (x) => x + '!'; let greet = COMPOSE(toLowerCase, exclaim); greet(“BOM DIA, WALKER”); // bom dia, walker!
Conclusão
Na prática, já utilizamos grande parte de todos os conceitos apresentados nesse post até mesmo involuntariamente ao trabalharmos com JavaScript. Isso porque as grandes bibliotecas como Jquery, UnderscoreJs, Lodash e outras utilizam os conceitos da programação funcional, além de também utilizar Currying, Compose entre muitas outras técnicas.
Se tem alguma dúvida crítica ou sugestão poste ai nos comentários!