Desenvolvimento

15 mar, 2019

Princípios de programação funcional com JavaScript

100 visualizações
Publicidade

Depois de um bom tempo aprendendo e trabalhando com programação orientada a objetos, comecei a pensar sobre complexidade em software.

  • “Complexity is anything that makes software hard to understand or to modify.” —  John Outerhout

Minha tradução não-oficial:

  • “Complexidade é qualquer coisa que faça o software ficar difícil de entender ou modificar”.

Li um artigo que também me fez refletir sobre complexidade e como podemos tentar reduzi-la para que nossos softwares fiquem mais manuteníveis: fáceis de fazer mudanças e adicionar código.

Pesquisando mais sobre complexidade de sistemas, achei os conceitos de programação funcional como imutabilidade e funções puras.

Esses conceitos possibilitam desenvolver funções sem efeitos colaterais. Logo, facilitam a manutenção de sistemas junto com outros benefícios.

Outra reflexão foi sobre o porquê de usarmos programação orientada a objetos, dado que ela gera grandes complexidades.

Neste artigo quero falar sobre programação funcional, apresentar alguns conceitos importantes e mostrar, na prática, como aplicá-los com JavaScript (sim, teremos muito código JavaScript aqui).

O que é programação funcional?

  • “Functional programming is a programming paradigm — a style of building the structure and elements of computer programs — that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.” — Wikipedia

Minha tradução não-oficial:

  • Programação funcional é um paradigma de programação – um estilo de construção de uma estrutura e elementos de programas de computador – que trata a computação como avaliação de funções matemáticas e evita mudança de estado e dados mutáveis”.

Também gosto muito da “definição” do Arthur Xavier sobre Programação Funcional:

  • “É um paradigma de programação onde computações são representadas por funções ou expressões puras, evitando efeitos colaterais e dados mutáveis e que utiliza amplamente de composição de funções e funções de primeira classe

E é exatamente sobre isso que vamos conversar neste artigo:

  • Paradigma de programação
  • Funções puras
  • Imutabilidade
  • Funções de primeira classe
  • Composição de funções

Observação: vamos conversar sobre functions chaining, mas toda a parte e conceitos de composição serão conversados em um próximo artigo. Spoiler: no próximo vamos conversar sobre mais utilizações de reduce, Closures, Curry e Function Composition.

Funções puras

O primeiro conceito fundamental que aprendemos quando queremos entender programação funcional são as funções puras. Mas o que isso significa? O que faz com que uma função seja pura?

Temos duas regras básicas para definição de pureza:

  • A função retorna sempre o mesmo resultado para um dado input (também conhecido como determinístico)
  • A função não causa nenhum efeito colateral

A função retorna sempre o mesmo resultado para um dado input

Imagine que queremos implementar uma função que calcula a área de um círculo. Uma função impura receberia um raio como parâmetro e calcularia a área: raio * raio * PI.

let PI = 3.14;

const calculateArea = (radius) => radius * radius * PI;

calculateArea(10); // returns 314.0

O calculateArea é uma função bem simples, mas por que é impura? Simplesmente porque ela usa um objeto global que não é passado como parâmetro na função.

Vamos imaginar que alguns matemáticos descobrem que o valor de PI é na verdade 42 e mudam o valor do objeto global.

Agora nossa função impura terá um resultado diferente: 10 * 10 * 42 = 4200. Para o mesmo parâmetro (radius = 10), temos um resultado diferente. Logo, ela não é determinística.

Agora, como consertamos isso? Como transformamos essa função impura em uma função pura, previsível e determinística?

let PI = 3.14;

const calculateArea = (radius, pi) => radius * radius * pi;

calculateArea(10, PI); // returns 314.0

Tudo o que precisamos fazer é passar o valor de PI como parâmetro da função. E agora temos acesso a todos os parâmetros sem precisar acessar um objeto externo.

  • Para os parâmetros radius = 10 e PI = 3.14, sempre vamos ter o mesmo resultado: 314.0
  • Para os parâmetros radius = 10 e PI = 42, sempre vamos ter o mesmo resultado: 4200

A composição dos parâmetros radius e PI sempre têm o mesmo resultado.

Outra solução é transformar nosso valor de PI em uma função (uma função é um valor, um dado. Vamos ver mais sobre isso mais pra frente).

const PI = () => 3.14;

const calculateArea = (radius) => radius * radius * PI();

calculateArea(10); // returns 314

Agora o PI é uma função, não apenas uma função qualquer. Ela é uma função pura.

A função calculateArea recebe radius como parâmetro e usa uma função pura, sem realizar nenhuma mutação. Logo, calculateArea se torna uma função pura também.

Lendo arquivos

Se nossa função lê arquivos externos, ela não é uma função pura – pelo simples motivo de que o conteúdo do arquivo pode mudar. Vamos ver esse exemplo a seguir:

const charactersCounter = (text) => `Character count: ${text.length}`;

function analyzeFile(filename) {
  let fileContent = open(filename);
  return charactersCounter(fileContent);
}

Imagine que chamamos nossa função analyzeFile passando o arquivo arq1.txt. Ela abre o arquivo, analisa e conta o número de caracteres.

Agora imagine que mudamos o conteúdo do arquivo. Para o mesmo arquivo arq1.txt como parâmetro, temos um resultado diferente, o que torna a nossa função não determinística, ou impura.

Um detalhe importante é a separação do “carregamento do arquivo” e da “contagem de texto”.

  • Carregamento do arquivo: função impura, pois está lidando com algo externo, que pode sofrer alterações
  • Contagem de texto: essa função é pura, dado que apenas recebe uma string e conta a quantidade de caracteres

Geração de números aleatórios

Qualquer função que depende de número aleatório não pode ser pura.

function yearEndEvaluation() {
  if (Math.random() > 0.5) {
    return "You get a raise!";
  } else {
    return "Better luck next year!";
  }
}

Depender de algo aleatório faz com que a função seja imprevisível. No entanto, funções puras são determinísticas e precisas. Com funções aleatórias, perdemos previsibilidade.

A função não causa nenhum efeito colateral

Alguns exemplos de efeitos colaterais incluem modificar um objeto global (alguma variável global, por exemplo) ou um parâmetro passado como referência.

Efeitos colaterais também são conhecidos como alterar o estado de um objeto, seja ele uma variável ou uma instância.

Para ilustrar essa mudança de estado, vamos implementar uma função que recebe um número inteiro e retorna o valor incrementado por 1.

let counter = 1;

function increaseCounter(value) {
  counter = value + 1;
}

increaseCounter(counter);
console.log(counter); // 2

Definimos a variável counter. E nossa função recebe esse valor e atribui um novo valor incrementado por 1.

Aqui estamos modificando o estado da variável counter, ou seja, nossa função é impura. Mas como fazemos para que nossa função seja pura? Ela simplesmente recebe o valor e retorna um valor incrementado por 1, sem a necessidade de mudança de estado.

let counter = 1;

const increaseCounter = (value) => value + 1;

increaseCounter(counter); // 2
console.log(counter); // 1

Nossa função pura increaseCounter retorna 2 e nossa variável counter mantêm o mesmo estado.

Benefícios de usar função puras

Se seguirmos essas duas regras simples (sem efeito colateral e determinístico), nossos programas ficam mais simples, mais fáceis de entender.

Funções puras são estáveis, consistentes e previsíveis. Para um mesmo parâmetro, as funções puras sempre retornam o mesmo resultado.

Outro grande benefício é o código ser testável mais facilmente. Não precisamos fazer mock. Apenas precisamos considerar diferentes contextos. Podemos fazer testes de unidade para diferentes contextos (parâmetros):

  • Dado um parâmetro A → esperasse que a função retorne o valor B
  • Dado um parâmetro C → esperasse que a função retorne o valor D

Um exemplo simples seria uma função que recebe uma lista de números e retorna um nova lista com cada número incrementado.

let list = [1, 2, 3, 4, 5];

const incrementNumbers = (list) => list.map(number => number + 1);

Nos detalhes de implementação, nossa função recebe uma lista de número inteiros, usa o a função map e retorna uma nova lista com cada número incrementado.

incrementNumbers(list); // [2, 3, 4, 5, 6]
  • Para a lista [1, 2, 3, 4, 5], o resultado esperado é [2, 3, 4, 5, 6]
  • Para a lista [0, 2, 4, 6, 8], o resultado esperado é [1, 3, 5, 7, 9]

Imutabilidade

Inalterável ao longo do tempo ou incapaz de ser alterado.

Quando dados são imutáveis, eles não sofrem alterações depois de serem criados. Se quisermos modificar um objeto imutável, ao invés de tentarmos modificar o seu valor, nós criamos um novo objeto com um novo valor. Assim, o antigo objeto imutável mantém seu estado inalterado.

Em JavaScript geralmente usamos o loop for para iterar em cima de listas.

Esse próximo código é um exemplo de código imperativo que possui algumas variáveis que são mutáveis ao longo do loop.

var values = [1, 2, 3, 4, 5];
var sumOfValues = 0;

for (var i = 0; i < values.length; i++) {
  sumOfValues += values[i];
}

sumOfValues // 15

Para cada iteração modificamos o valor da variável i e de sumOfValue. Queremos aplicar imutabilidade agora, mas como podemos fazer a iteração sem modificar o estado das nossas variáveis? Recursão é a resposta!

let list = [1, 2, 3, 4, 5];
let accumulator = 0;

function sum(list, accumulator) {
  if (list.length == 0) {
    return accumulator;
  }

  return sum(list.slice(1), accumulator + list[0]);
}

sum(list, accumulator); // 15
list; // [1, 2, 3, 4, 5]
accumulator; // 0

Aqui temos a função sum que recebe uma lista de número e um acumulador que será o resultado final da soma (inicializado como 0).

Primeiro temos o caso base (quando a lista está vazia): se ela estiver vazia, retornamos o valor do acumulador, que basicamente é nossa soma.

Enquando a lista não estiver vazia, a função chama ela própria sempre passando o valor “atualizado” do acumulador.

Com recursão, conseguimos manter nossas variáveis imutáveis. O estado da lista (list) e do acumulador (accumulator) se mantém inalterado – imutabilidade for the win.

Observação: podemos usar reduce para implementar essa função sum, mas vamos aprender como fazer isso apenas no tópico sobre Funções de Alta Ordem.

Um exemplo mais complexo de imutabilidade

É bem comum implementarmos um código que constrói o estado final de um objeto. Vamos construir uma classe que transforma uma string em uma url slug.

Em programação orientada a objetos em Ruby, criaríamos uma classe UrlSlugify, e essa classe teria um método slugify que transforma a string na url slug.

class UrlSlugify
  attr_reader :text
  
  def initialize(text)
    @text = text
  end

  def slugify!
    text.downcase!
    text.strip!
    text.gsub!(' ', '-')
  end
end

UrlSlugify.new(' I will be a url slug   ').slugify! # "i-will-be-a-url-slug"

Feito! Essa implementação é um código imperativo que fala exatamente como o algoritmo do slugify deve funcionar: transforma a string em “caixa baixa”, remover os espaços em branco do começo e do fim e troca os espaços em branco por hifens.

Mas para esse caso, estamos mutando o estado da string. Para ser mais preciso, a mutação ocorre em três etapas.

Conseguimos remover essa mutação implementando composição de funções (function composition) ou encadeamento de funções (function chaining).

Em outras palavras, o resultado de uma função é usado como input para próxima função, sem modificar o estado da string.

let string = " I will be a url slug   ";

function slugify(string) {
  return string.toLowerCase()
    .trim()
    .split(" ")
    .join("-");
}

slugify(string); // i-will-be-a-url-slug

Aqui temos:

  • toLowerCase: converte a string para caixa baixa
  • trim: remove os espaços em branco do começo e final da string
  • split e join: troca todos os espaços por hífen

Combinamos essas quatro funções e podemos transformar nossa string em uma url slug sem ter efeitos colaterais.

Lembrando que esses quatro métodos de string não alteram o estado da string. Basicamente eles recebem o valor da string e retornam uma copia modificada, porém, o estado e a referência continuam os mesmos. Imutabilidade do estado.

Transparência referencial

Vamos implementar uma função square que calcula o quadrado de um número:

const square = (n) => n * n;

Essa função pura sempre tem o mesmo resultado para a mesma entrada.

square(2); // 4
square(2); // 4
square(2); // 4
// ...

Passando 2 como parâmetro da função square sempre terá o mesmo resultado 4. Dado que sempre teremos o mesmo resultado, podemos trocar o square(2) pela constante 4. Nossa função é referencialmente transparente.

  • Funções puras + dados imutáveis = transparência referencial

Com esse conceito de transparência referencial, uma coisa legal que podemos fazer é usar memorization. Como exemplo, temos uma função de soma sum:

const sum = (a, b) => a + b;

Agora chamamos a função com esses parâmetros:

sum(3, sum(5, 8));

A soma sum(5, 8) é igual a 13. Essa função sempre tem o mesmo resultado: 13. Então podemos “memorizar” esse resultado e usar da seguinte forma:

sum(3, 13);

Funções como entidades de primeira classe

Funções como entidades de primeira classe é a ideia de que funções são tratadas como valores e usados como dados.

Funções como entidades de primeira classe podem:

  • Ter suas referências “armazenadas” em variáveis e constantes
  • Ser passadas como parâmetro em outras funções
  • Ser retornadas como resultado de outras funções

Como resumo, a ideia é tratar nossas funções como dados. Dessa forma, podemos combinar diferentes funções para criar novas funções com um novo comportamento.

Confuso?

Imagine que temos essa função que soma dois valores e depois dobra o valor. Algo assim:

const doubleSum = (a, b) => (a + b) * 2;

Agora queremos implementar uma mesma função, mas subtrair os valores, ao invés de somá-los:

const doubleSubtraction = (a, b) => (a - b) * 2;

Essas duas funções possuem lógicas bem similares. A única diferença são os operadores matemáticos.

Se tratarmos funções como valores, podemos extrair a lógica dos operadores como funções e passá-los como parâmetros.

const sum = (a, b) => a + b;
const subtraction = (a, b) => a - b;

const doubleOperator = (f, a, b) => f(a, b) * 2;

doubleOperator(sum, 3, 1); // 8
doubleOperator(subtraction, 3, 1); // 4
  • Implementamos a função sum que recebe dois valores e soma.
  • Implementamos a função subtraction que recebe dois valores e subtrai.
  • Agora nossa função doubleOperator recebe uma função e usa para fazer a operação antes de dobrar seu valor.

Agora conseguimos usar o doubleOperator passando as funções sum ou subtraction, podendo extrapolar para outras funções. Por exemplo: multiplicação.

Assim criamos um novo comportamento, dependendo da função que o doubleOperator recebe.

Funções de alta ordem

Quando falamos sobre funções de alta ordem, queremos dizer funções que:

  • Recebem uma ou mais funções como parâmetros

Ou:

  • Retornam função como resultado

A função doubleOperator que implementamos acima é uma função de alta ordem porque recebe uma função como argumento e usa ela para criar um novo comportamento.

Você provavelmente já ouviu falar sobre filter, map e reduce. Vamos entender elas mais a fundo – dados que são as principais funções de alta ordem.

Filter

Dada uma lista, queremos filtrar por algum “parâmetro”. A função filter espera um valor true ou false para determinar se o elemento deve ou não ser incluído no resultado final.

Basicamente, se a função de callback retorna true, a função filter inclui o elemento na lista final. Caso contrário, ela “remove” esse elemento.

Um exemplo simples é quando temos uma lista de inteiros e queremos apenas os número pares.

Uma abordagem imperativa

Uma maneira imperativa de implementar essa função em JavaScript, é:

  • Criar uma lista vazia evenNumbers
  • Iterar pela nossa lista numbers
  • Verificar os números que são pares e adicioná-los na lista evenNumbers
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var evenNumbers = [];

for (var i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 == 0) {
    evenNumbers.push(numbers[i]);
  }
}

console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]

Uma abordagem declarativa

Podemos usar a função filter que apenas recebe uma função even e retorna a lista de números pares:

const even = n => n % 2 == 0;
const listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10]

Usamos uma simples arrow function para definir nossa função even e já podemos usá-la na função filter.

Ao invés de falarmos exatamente como o algoritmo deve filtrar essa lista, apenas declaramos que queremos os números pares.

Um problema interessante que resolvi no path do Hacker Rank FP foi o problema Filter Array. Basicamente, a ideia do problema é que dada uma lista de números inteiros, precisamos filtrar apenas números que são menores que um valor especificado X.

Abordagem imperativa

Uma maneira imperativa de se resolver esse problema, é:

var filterArray = function(x, coll) {
  var resultArray = [];

  for (var i = 0; i < coll.length; i++) {
    if (coll[i] < x) {
      resultArray.push(coll[i]);
    }
  }

  return resultArray;
}

console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0]

Falamos exatamente como a nossa função filtrar  – dada a lista e o valor de x, iteramos nossa lista de inteiros e para cada número menor que x, adicionamos na lista, que será o resultado final.

Abordagem declarativa

Mas agora vamos resolver esse mesmo problema de uma forma mais declarativa, usando a função de alta ordem filter.

const isSmaller = x => element => element < x;

const filterArray = function(x, list) {
  return list.filter(isSmaller(x));
}

const numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0];
filterArray(3, numbers); // [2, 1, 0]

Aqui usamos closure para definir qual o x dentro da função isSmaller. Nossa função isSmaller recebe x e retorna uma nova função element => element < x.

Assim, para cada elemento da lista de inteiros, conseguiremos verificar quais são os números menores que x.

Podemos fazer a mesma implementação para a lista de objetos JavaScript. Imagine que temos uma lista de pessoas que possui atributos como name e age:

let people = [
  { name: "TK", age: 26 },
  { name: "Kaio", age: 10 },
  { name: "Kazumi", age: 30 }
];

E agora queremos filtrar pessoas que tenham idade maior que valor específico. Nesse caso, pessoas com idade maior que 21 anos.

const olderThan21 = person => person.age > 21;
const overAge = people => people.filter(olderThan21);
overAge(people); // [{ name: 'TK', age: 26 }, { name: 'Kazumi', age: 30 }]

Resumo do código:

  • Temos uma lista de pessoas com atributos name e age
  • Temos a função olderThan21. Nesse caso a função implementa a lógica de verificar se a idade (age) de determinada pessoa é maior que 21
  • E finalmente filtramos todas as pessoas baseados na função olderThan21

Map

A ideia da função map é transformar cada elemento de uma lista.

  • A função map transforma uma lista aplicando uma função em todos os seus elementos e construindo uma nova lista com o valores retornados

Vamos pegar o mesmo exemplo da lista people, mas agora não queremos filtrar as pessoas. Queremos apenas retornar uma lista de strings, algo como TK is 26 years old, então a string final seria ${name} is ${age} years old, onde o name e o age são atributos de cada objeto da lista people.

Um maneira imperativa de se resolver esse problema, é:

var people = [
  { name: "TK", age: 26 },
  { name: "Kaio", age: 10 },
  { name: "Kazumi", age: 30 }
];

var peopleSentences = [];

for (var i = 0; i < people.length; i++) {
  var sentence = people[i].name + " is " + people[i].age + " years old";
  peopleSentences.push(sentence);
}

console.log(peopleSentences); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']

Definimos uma nova lista vazia, que será o resultado final com nossas strings, iteramos a lista people: para cada pessoa, montamos a sentença com os atributos name e age, e adicionamos na lista final.

Um jeito declarativo é usar o map:

const makeSentence = (person) => `${person.name} is ${person.age} years old`;

const peopleSentences = (people) => people.map(makeSentence);
  
peopleSentences(people);
// ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']

A ideia é transformar uma lista (de objetos) em uma nova lista (de strings).

  • makeSentence: interpola os atributos name e age para formar a string desejada
  • peopleSentences: faz um map para retornar a lista desejada, passando a função makeSentence, aplicando em cada objeto pessoa da lista

Outro problema interessante do Hacker Rank é o update list problem. O problema é o seguinte: dada uma lista de números, queremos uma nova lista com os valores absolutos de cada número.

Um input [1, 2, 3, -4, 5] resultará no output [1, 2, 3, 4, 5]. Por que?

  • O valor absoluto de 1 é 1
  • O valor absoluto de -4 é 4

E assim por diante.

Uma maneira de resolver esse problema é para cada número da lista, fazer uma atualização “in-place” dos valores. Ou seja, modificar todos os números pelo seu valor absoluto.

var values = [1, 2, 3, -4, 5];

for (var i = 0; i < values.length; i++) {
  values[i] = Math.abs(values[i]);
}

console.log(values); // [1, 2, 3, 4, 5]

Então iteramos em cima da lista e para cada elemento usamos a função Math.abs para transformar nosso número em seu valor absoluto e depois fazemos a atualização in-place.

Agora vamos pensar um pouco sobre essa implementação: ela não é uma solução funcional (aplicadas nos conceitos de programação funcional). Por que?

Lembra que aprendemos sobre imutabilidade? Agora sabemos que imutabilidade é super importante para fazer com que nossas funções sejam consistentes e previsíveis.

Basicamente, a ideia é construir uma nova lista com todos os valores absolutos.

Outra reflexão é sobre o código imperativo. Por que não deixamos nosso código mais declarativo usando a função map para transformar todos os nossos dados?

Pensando nesses dois pontos, vamos implementar uma nova solução. Minha primeira ideia era testar o funcionamento da função Math.abs.

Math.abs(-1); // 1
Math.abs(1); // 1
Math.abs(-2); // 2
Math.abs(2); // 2

Como queremos uma solução que preze pela imutabilidade de dados e declaratividade do código, vamos usar a função map para lidar com a nossa lista.

  • map: vou receber a lista e transformar todos os valores em valores absolutos.

Então nossa função map precisa apenas receber uma função que transforma valores em seus valores absolutos. Lembra que testamos a funcionalidade da função Math.abs? Podemos usá-la dentro do map.

let values = [1, 2, 3, -4, 5];

const updateListMap = (values) => values.map(Math.abs);

updateListMap(values); // [1, 2, 3, 4, 5]

Agora nossa função updateListMap ficou super simples. Contempla três itens:

  • values: lista de números
  • map: função de alta ordem que recebe uma função para fazer transformações em cima de uma lista
  • Math.abs: função que tranforma valores em valores absolutos

Lindo!

Reduce

A ideia da função reduce é receber uma função e uma lista e retornar um valor combinando todos os itens da lista.

Um exemplo simples para ilustrar o funcionamento do reduce é calcular o preço total de um pedido.

Imagine que temos um site do tipo e-commerce. Adicionamos o Product 1, Product 2, Product 3 e Product 4 no carrinho de compras (nosso pedido). Agora queremos calcular o preço total desse pedido.

Uma maneira imperativa seria iterar pela de lista de pedidos e somar o preço de cada produto em uma variável totalAmount.

var orders = [
  { productTitle: "Product 1", amount: 10 },
  { productTitle: "Product 2", amount: 30 },
  { productTitle: "Product 3", amount: 20 },
  { productTitle: "Product 4", amount: 60 }
];

var totalAmount = 0;

for (var i = 0; i < orders.length; i++) {
  totalAmount += orders[i].amount;
}

console.log(totalAmount); // 120

Definimos nossa variável totalAmount, iteramos em cima da lista de pedidos e somamos cada preço na nossa variável. Simples.

Agora vamos para uma implementação mais declarativa. Usaremos a função reduce para realizar essa soma.

let shoppingCart = [
  { productTitle: "Product 1", amount: 10 },
  { productTitle: "Product 2", amount: 30 },
  { productTitle: "Product 3", amount: 20 },
  { productTitle: "Product 4", amount: 60 }
];

const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount;

const getTotalAmount = (shoppingCart) => shoppingCart.reduce(sumAmount, 0);

getTotalAmount(shoppingCart); // 120

Primeiro temos o shoppingCart que é o nosso pedido com todos os produtos.

A API da função reduce é a seguinte: ela vai esperar dois valores:

  • fn (recebe 4 valores): o acumulador, o elemento atual da lista, o index atual (opcional) e a lista (opcional)
  • valor inicial: esse parâmetro é opcional. Se não for passado, o valor inicial será o primeiro elemento da lista

Vamos começar pelo mais simples. O valor inicial que queremos é 0, dado que queremos somar todos os preços dos produtos.

E agora a função (também chamada de reducer) que lidará com a soma dos preços. Criamos a função sumAmount. Ela recebe dois valores: o currentTotalAmount (nosso acumulador — e soma final) e o order (pedido na lista de pedidos).

O retorno dessa função é o valor do acumulador na próxima iteração. Então, basicamente pegamos o valor do total atual (currentTotalAmount) e somamos com o preço do produto.

Resumindo, a função getTotalAmount é usada para somar os preços de cada produto de shoppingCart, aplicando a função sumAmount e começando por 0.

Outra forma de pegar o preço total de todos os produtos é compor as funções map e reduce. O que queremos dizer com isso?

Podemos usar a função map para transformar o nosso shoppingCart em uma lista de preços.

E depois usar a função reduce apenas para somar essa lista de preços

const getAmount = (order) => order.amount;
const sumAmount = (acc, amount) => acc + amount;

function getTotalAmount(shoppingCart) {
  return shoppingCart
    .map(getAmount)
    .reduce(sumAmount, 0);
}

getTotalAmount(shoppingCart); // 120

Então, nessa nova implementação temos o map que recebe a função getAmount para transformar todos os pedidos em preços.

Depois o reduce recebe sumAmount, que apenas precisa somar o acumulador com o valor da iteração atual (que é um preço, e não mais um produto), inicializando com 0.

Compondo filter, map e reduce

Agora que entendemos como cada função de alta ordem funciona, vamos ver um exemplo que usa a composição das três funções.

Ainda pensando naquele exemplo do shopping cart, vamos considerar que ele também tenha o atributo type em seu objeto. Ficaria assim:

let shoppingCart = [
  { productTitle: "Functional Programming", type: "books", amount: 10 },
  { productTitle: "Kindle", type: "eletronics", amount: 30 },
  { productTitle: "Shoes", type: "fashion", amount: 20 },
  { productTitle: "Clean Code", type: "books", amount: 60 }
]

O problema que queremos resolver é o seguinte: como categorizamos o preço total pelo tipo de produto? Ou seja, a ideia é: dada a lista de produtos, queremos saber quanto é o preço total de produtos do tipo books, por exemplo.

Como o algoritmo vai funcionar?

  • filter: filtrar por o type books
  • map: transformar a lista de pedidos filtrada em uma nova lista com apenas os preços dos produtos
  • reduce: acumular todos os itens da lista somando todos os valores
let shoppingCart = [
  { productTitle: "Functional Programming", type: "books", amount: 10 },
  { productTitle: "Kindle", type: "eletronics", amount: 30 },
  { productTitle: "Shoes", type: "fashion", amount: 20 },
  { productTitle: "Clean Code", type: "books", amount: 60 }
]

const byBooks = (order) => order.type == "books";
const getAmount = (order) => order.amount;
const sumAmount = (acc, amount) => acc + amount;

function getTotalAmount(shoppingCart) {
  return shoppingCart
    .filter(byBooks)
    .map(getAmount)
    .reduce(sumAmount, 0);
}

getTotalAmount(shoppingCart); // 70

A implementação fica bem simples! Temos três arrow functions separadas, que serão usadas em nossas funções de alta ordem: byBooks, getAmount e sumAmount.

A função getTotalAmount recebe os pedidos (shoppingCart), filtra, transforma e soma todos os valores.

Recursos

Organizei alguns conteúdos que li e estudei em um repositório do GitHub: Functional Programming Github repository.

Fiquem à vontade para colaborar com links e conteúdo que possa ajudar a comunidade nos estudos de programação funcional.

Espero que tenha sido divertido aprender mais sobre programação funcional! Esse artigo foi uma tentativa de compartilhar o que tenho aprendido e que pode ser útil para desenvolvedoras/desenvolvedores. Te vejo no próximo artigo!

Referências

Introduções à programação funcional

Funções puras

Dados imutáveis

Funções de alta ordem

Programação Declarativa

Se você curte videos

Livros

Outros recursos que recomendo