Desenvolvimento Programação funcional

Conceitos de linguagens funcionais: uma breve introdução aos monads

Neste post, dando continuidade à série sobre programação funcional, trataremos de um tema muito pertinente: os monads e suas definições essenciais.

há 4 anos 10 meses


Você sabia que a TreinaWeb é a mais completa escola para desenvolvedores do mercado?

O que você encontrará aqui na TreinaWeb?

Conheça os nossos cursos

O paradigma funcional vem ganhando cada vez mais espaço entre os desenvolvedores, quer seja pela sua naturalidade para lidar com problemas cada vez mais comuns em softwares de larga escala (como os aspectos de concorrência e paralelismo) ou por causa da sintaxe mais declarativa que as linguagens funcionais geralmente possuem. Mas, mesmo com essa popularização, o paradigma funcional ainda é visto com certo receio por uma série de razões, como a proximidade com a matemática (odiada por muitas pessoas) ou a grande quantidade de termos encarados como obscuros para quem não conhece o paradigma. Por isso, iniciei essa série de artigos abordando conceitos e termos típicos do paradigma funcional, tentando mostrar como as coisas podem ser muito mais simples do que parecem. O primeiro artigo dessa série discute sobre os princípios mais essenciais do paradigma funcional. Já o segundo artigo, onde o currying é discutido, pode ser acessado a partir daqui. Neste artigo, vamos discutir um pouco sobre um conceito essencial ao paradigma funcional: os monads.

Relembrando alguns conceitos…

Antes de prosseguirmos aos monads de fato, vamos relembrar alguns pilares do paradigma funcional:

  • O paradigma funcional tem forte inspiração nos conceitos relacionados às funções matemáticas;
  • Um código escrito de maneira funcional é composto por funções que sempre vão receber ao menos um parâmetro e produzir uma saída;
  • No paradigma funcional, as funções devem ser determinísticas, ou seja: dado um valor para um parâmetro, a saída produzida deve ser sempre a mesma em qualquer momento e situação. Por exemplo: se temos uma função somar() e esta função recebe os parâmetros 2e 2, esta função deve sempre retornar 4, independente se ela for invocada hoje, amanhã, ano que vem ou no próximo milênio;
  • Linguagens funcionais evitam o que é chamado side effect ou transições de estado: quando você cria uma “variável” (geralmente chamada de binding em programação funcional), esta “variável” não pode ter seu valor alterado em nenhuma circunstância. A grosso modo, é como se você estivesse trabalhando o tempo inteiro com “constantes de verdade”.

Sendo assim, vamos imaginar que necessitamos de alguma estrutura de código que execute uma operação de elevação de um número ao cubo… Se adotarmos o JavaScript como linguagem, poderíamos criar um “método” que executasse essa operação:

const cubo = (n) => Math.pow(n, 3);

// Testes...
console.log(cubo(2)); // Resposta: 8

Por hora, tudo certo… Temos um trecho de código que segue todos os pilares do paradigma funcional. Também poderíamos ter uma função que calculasse a raiz quadrada de um número… O código seria bem similar:

const raizQuadrada = (n) => Math.sqrt(n);

// Testes...
console.log(raizQuadrada(4)); // Resposta: 2

Por alguma razão, poderíamos precisar calcular a raiz quadrada de um número que foi elevado ao cubo… No caso acima, como ambas as funções recebem um parâmetro numérico e retornam um número, nós poderíamos encadear as chamadas destas funções, criando uma nova função como resultado da combinação das duas funções anteriores:

const raizQuadrada = (n) => Math.sqrt(n);
const cubo = (n) => Math.pow(n, 3);

const raizQuadradaDoCubo = (n) => raizQuadrada(cubo(n));

// Testes...
console.log(raizQuadradaDoCubo(4)); // Resposta: 8 (4 elevado ao cubo = 64; raiz de 64 = 8)

Esse código ficaria mais interessante ainda se criássemos uma função que fosse responsável exclusivamente por realizar essa composição de funções… Poderíamos ter algo similar ao trecho de código abaixo:

const juntar = (f, g) => (x) => f(g(x));

const cubo = (x) => Math.pow(x, 3);
const raizQuadrada = (x) => Math.sqrt(x);
const cuboDaRaizQuadrada = juntar(cubo, raizQuadrada);

// ...
console.log(cuboDaRaizQuadrada(4)); // Resultado: 8

F# (F Sharp) - Fundamentos
Curso F# (F Sharp) - Fundamentos
Conhecer o curso

O trecho de código acima implementa quase tudo que vimos sobre programação funcional: tudo está baseado em valores imutáveis, temos várias funções que recebem pelo menos um parâmetro e retornam um determinado valor relacionado ao processamento do parâmetro, as funções são todas determinísticas e até temos o currying em ação aqui, já que a função juntar() retorna uma outra função, que é o resultado da composição das funções f() e g()! E veja a estrutura declarativa do código: o código é fluído e fácil de ser lido… Mas esse código tem um problema crítico: o que aconteceria se passarmos undefined como argumento da função cuboDaRaizQuadrada()? Nesse caso, vamos acabar tendo o famoso NaN, o que é completamente errado. Podemos corrigir isso colocando uma verificação de segurança nas funções cubo() e raizQuadrada():

const juntar = (f, g) => (x) => f(g(x)); 

const cubo = (x) => {
  if (x) {
    return Math.pow(x, 3)
  } else {
    return 0;
  }
};

const raizQuadrada = (x) => {
  if (x) {
    Math.sqrt(x);
  } else {
    return 0;
  }
}

const cuboDaRaizQuadrada = juntar(cubo, raizQuadrada);

// ...
console.log(cuboDaRaizQuadrada(undefined)); // Resultado: 0

Isso resolve o nosso problema. Mas já temos código idêntico espalhado entre vários lugares… E cada nova função que implementássemos, precisaria replicar essa verificação. Essa situação é terrível, quer seja pela legibilidade (que já começa a ficar prejudicada), quer seja pela manutenibilidade desse trecho simples de código… Nós precisamos de uma estrutura que encapsule esse processo de verificação que necessitamos… E é aqui que começam a entrar os monads.

Implementando o primeiro monad

Poderíamos escrever uma estrutura que encapsulasse esse valor e nos disesse se temos um valor válido em questão. Essa estrutura poderia ser definida da seguinte maneira:

const _talvez = (valor) => ({
    valor,
    nada() {
      return valor == null || valor == undefined;
    }
});

const x = _talvez(4);
console.log(x.valor); // 4
console.log(x.nada()); // false
const y = _talvez(undefined);
console.log(y.valor); // undefined
console.log(y.nada()); // true

Agora, temos a estrutura _talvez que adiciona comportamentos ao objeto valor, fazendo com que seja possível:

  • Encapsular o valor internamente, o expondo adequadamente através da propriedade valor;
  • Tratar o valor que é encapsulado, o identificando como um valor válido ou não através da função nada().

Essa é a definição mais essencial de um monad: uma estrutura que age como um wrapper em cima de um dado, expondo seu valor de maneira correta e controlando possíveis side-effects que sua manipulação pode causar (como o NaNque não esperávamos). Nossa estrutura _talvez ainda precisa implementar pelo menos mais uma funcionalidade para chegar mais próxima ainda da definição clássica de um monad: o encadeamento de operações no valor monádico. Ou seja: um monad precisa prover uma maneira de estabelecer uma sequência de operações que deve ser aplicada ao valor que ele guarda (em nosso exemplo, o valor contido em valor dentro da estrutura _talvez). Isso poderia ser implementado da seguinte maneira:

const _talvez = (valor) => ({
    valor,
    nada() {
      return valor == null || valor == undefined;
    },
    ligar(fn) {
      if (this.nada()){
        return _talvez(null);
      } else {
        return _talvez(fn(this.valor));
      }
    }
});

Desenvolvedor Especialista Front-end
Formação Desenvolvedor Especialista Front-end
Conhecer a formação

Veja que agora criamos mais uma função em nossa estrutura _talvez. A função ligar() recebe uma função que pode ser aplicada ao valor armazenado pela estrutura _talvez, desde que este seja considerado um valor válido (ou seja, que a função nada() retorne false). Se a função puder ser aplicada, um nova estrutura _talvez (ou seja, um novo monad) é gerado, permitindo a ligação com outras funções. Veja que, caso tenhamos um valor monádico inválido (ou seja, nada() retornar true), um novo monad também é gerado, mas com o valor null, já que temos um valor inválido. Isso é chamado de automatic propagation. Agora, podemos reescrever o nosso código que calcula a raiz de um número elevado ao cubo da seguinte forma, utilizando a nossa estrutura _talvez:

const juntar = (f, g) => (x) => f(g(x)); 

const _talvez = (valor) => ({
    valor,
    nada() {
      return valor == null || valor == undefined;
    },
    ligar(fn) {
      if (this.nada()){
        return _talvez(null);
      } else {
        return _talvez(fn(this.valor));
      }
    }
});

const cubo = (monad) => monad.ligar(n => Math.pow(n, 3));
const raizQuadrada = (monad) => monad.ligar(n => Math.sqrt(n));
const cuboDaRaizQuadrada = juntar(cubo, raizQuadrada);

const x = _talvez(4);
console.log(cuboDaRaizQuadrada(x).valor); // 8
const y = _talvez(null);
console.log(cuboDaRaizQuadrada(y).valor); // null
const z = _talvez(undefined);
console.log(cuboDaRaizQuadrada(z).valor); // null

Veja que nosso monad evitou um possível side-effect (que poderia acontecer com o NaN que podia ser retornado), além de ainda seguir todos os princípios funcionais vistos anteriormente. Nossa estrutura _talvez é de fato bem próxima a um monad. Ah! Reparou a semelhança entre o método ligar() da estrutura _talvez com métodos comuns do JavaScript como o map()? Essa semelhança não acontece por acaso… Uma outra observação pertinente: se você já teve contato com alguma linguagem funcional (como F#, Haskell, Elixir, Scala, entre outras), vai também reparar que a nossa função juntar(), utilizada para compor funções, faz exatamente a mesma coisa que o pipe operator (|>) das linguagens funcionais. ;)

Eu realmente preciso conhecer monads?

A resposta é… Depende. Se você não utiliza linguagens funcionais, não tem muito sentido estudar monads, tendo em vista que outros paradigmas (como o paradigma orientado a objetos) já quebram o conceito de monads só por se basearem em transições de estados. Eles podem até ajudar a reduzir a complexidade do código, mas outros paradigmas podem oferecer outras soluções mais interessantes através de outros design patterns. Se você utiliza alguma linguagem funcional, a resposta ainda é depende. Existem linguagens que implementam o conceito de maneira mais “suave” por considerar o conceito e o termo muito “agressivos” ou obscuros demais, escondendo algumas complicações do desenvolvedor (é o caso do F# por exemplo, que dá o nome de workflow e computed expression a estruturas similares aos monads). Outras linguagens já exibem os conceitos de monads de maneira bem explícita, como o Haskell. Com certeza será interessante para qualquer desenvolvedor que trabalhe com o paradigma funcional conhecer pelo menos os aspectos essenciais dos monads, mas a profundidade deste estudo fatalmente estará atrelada à linguagem utilizada. Monads constituem um conceito de fato mais complexo dentro do paradigma funcional. Tanto que ainda existem mais regras matemáticas que devem ser aplicadas para que determinada estrutura seja de fato considerada um monad. Mas, neste momento, não iremos entrar em uma discussão mais aprofundada sobre a definição dos monads. Porém, mesmo assim, não se trata de algo impossível ou mirabolante como o nome do termo pode sugerir: trata-se na verdade de uma estrutura poderosíssima que permite ao paradigma funcional continuar aplicando seus conceitos básicos em situações como no caso de imprevisibilidade de valores de entrada. De quebra, os monads ainda constituem um tipo de estrutura que não precisa necessariamente ficar restrita ao universo funcional, podendo ser aplicada até mesmo na programação orientada a objetos para encapsulamento de dados e simplificação de código.

Autor(a) do artigo

Cleber Campomori
Cleber Campomori

Líder de Conteúdo/Inovação. Pós-graduado em Projeto e Desenvolvimento de Aplicações Web. Certificado Microsoft MCSD (Web).

Todos os artigos

Artigos relacionados Ver todos