Desenvolvimento

13 out, 2016

Programação funcional avançada: monads em JavaScript

Publicidade

Você já percebeu que cada vez mais o termo programação funcional vem sendo usado pela comunidade?

No meu último artigo, Entendendo Programação Funcional em JavaScript de uma vez, expliquei os conceitos mais básicos. Porém, este artigo vai mostrar os detalhes mais técnicos pra quem quer se aprofundar e entender de vez o assunto.

Então, continue lendo este texto para aprender:

1. Composição de funções
2. Pointfree functions
3. Utilizar pipes com o Pareto.js
4. O que são Monads

monad-1

 Composição de funções

Se você já entendeu o básico da programação funcional, percebeu que um dos principais objetivos é construir funções pequenas (e puras) para que possamos compô-las e desenvolver outras funções maiores e mais complexas.

Com isso em mente, vamos construir uma solução funcional para gerar um slug simples de uma string:

slug('Novo Post No Blog') // novo-post-no-blog

Ou seja, recebemos uma string como parâmetro e retornamos uma transformação dessa string: 1) aplicamos um lowercase e 2) fazemos a substituição de espaços por traços.

O código simples abaixo resolve esse problema:

const toLowerCase = s => s.toLowerCase()
const replaceSpaces = s => s.replace(/\s+/g, '-')
const slug = title => replaceSpaces(toLowerCase(title))

slug('Novo Post No Blog'); // novo-post-no-blog

A função slug, que é a função principal, recebe uma string title como parâmetro, depois aplica a função toLowerCase em title e retorna esse valor para que a função replaceSpaces retorne a nova string transformada.

Desta forma:

slug('Novo Post No Blog');
  1. toLowerCase('Novo Post No Blog'); 
  2. replaceSpaces('novo post no blog');
  3. 'novo-post-no-blog'

Em resumo, a função slug é uma composição de funções.

Apesar de já estarmos num caminho mais funcional, ainda temos alguns problemas com a função slug. A legibilidade é um deles:
const slug = title => replaceSpaces(toLowerCase(title))

Lemos a função da esquerda para a direita:

1. replaceSpaces
2. toLowerCase

Só que a função slug, como mostrei anteriormente, aplica essas duas funções na ordem contrária da forma que lemos.

Como resolver esse problema?

Pipes

Composição de funções é algo tão comum ao usar o paradigma funcional, que um padrão comum para fazer essa composição é usar pipes.

O JavaScript não vem com uma função pipe na própria linguagem, portanto vamos usar uma lib funcional para isso. Poderíamos usar o Ramda.js sem nenhum problema, mas nesse exemplo vou usar o Pareto.js, que é bem semelhante ao Ramda.js, só que é lightweight e mais moderno.

Com a função pipe então, o problema estaria resolvido desta forma:

import { pipe } from 'paretojs'

const toLowerCase = s => s.toLowerCase()
const replaceSpaces = s => s.replace(/\s+/g, '-')

const slug = pipe(toLowerCase, replaceSpaces)


Assim, atingimos o primeiro objetivo, que era melhorar a ordem da leitura da função, que agora está mais natural: da esquerda para a direita.

Além disso, conseguimos também uma outra vantagem mais sutil.

Leia novamente as duas versões da função slug:

//versão antiga
const slug = title => replaceSpaces(toLowerCase(title))

// versão nova
const slug = pipe(toLowerCase, replaceSpaces)

A nova versão não contém nenhuma referência ao parâmetro title que será passado. Isso tem um nome: pointfree function.

Com isso, não ficamos tão acoplados a um nome específico do parâmetro e temos uma função mais flexível a possíveis mudanças e mais fácil de ler.

Null

A nossa função slug funciona muito bem, só tem um problema: no mundo real, eventualmente passamos null como parâmetro. Nesse caso, a função quebraria e nós nem saberíamos o que aconteceu.

Podemos resolver de uma forma simples:

const toLowerCase = s => {
  if (s !== null) {
    return s.toLowerCase()
  }
}

const replaceSpaces = s => {
  if (s !== null) {
    return s.replace(/\s+/g, '-')
  }
}

Mas essa solução não é muito escalável: precisamos colocar a verificação de null em todas nossas funções, que deveriam ser simples e focadas em resolver apenas um problema.

Seria bem mais simples centralizar essa checagem em um único lugar.

Monads

Um Monad, resumidamente, é simplesmente um wrapper de um valor qualquer. A função Maybe abaixo faz exatamente isso:

function Maybe(value) {
  return {
    value: value
  }
}

const maybeString = Maybe('Novo Post No Blog')
maybeString.value // 'Novo Post No Blog'

const maybeNull = Maybe(null)
maybeNull.value // null

Com isso, já temos um wrapper, mas ainda faltam alguns detalhes.

Primeiramente, vamos fazer um refactoring na função Maybe, para tirar proveito de alguns benefícios do ES6.

Então, vamos transformar isto:

// ES5
function Maybe(value) {
  return {
    value: value
  }
}

Nisto:

// ES6
const Maybe = value => ({
  value
})

Como nosso principal objetivo com os Monads é ter uma garantia em relação ao null, vamos adicionar uma função isNothing que faz essa verificação:

const Maybe = value => ({
  value,
  isNothing() {
    return (this.value === null)
  }
})

const maybeString = Maybe('Novo Post No Blog')
maybeString.isNothing() // false

const maybeNull = Maybe(null)
maybeString.isNothing() // true

Pra finalizar nosso Monad, precisamos de uma nova função que:

1. Aplique outra função ao valor do wrapper, caso esse valor exista
2. Não faça nada caso o valor seja null

const Maybe = value => ({
  value,
  isNothing() {
    return (this.value === null)
  },
  map(fn) {
    if (this.isNothing()) {
      return Maybe(null)
    }
    return Maybe(fn(this.value))
  }
})

const toLowerCase = s => s.toLowerCase()

const maybeString = Maybe('Novo Post No Blog')
maybeString.map(toLowerCase) // Maybe {value: 'novo post no blog'}

const maybeNull = Maybe(null)
maybeNull.map(toLowerCase) // Maybe {value: null}

Agora que temos a versão finalizada do Maybe Monad, precisamos apenas atualizar a versão inicial da função slug:

import { pipe } from 'paretojs'

const toLowerCase = maybeStr => maybeStr.map(s => s.toLowerCase())
const replaceSpaces = maybeStr => maybeStr.map(s => s.replace(/\s+/g, '-'))

const slug = pipe(toLowerCase, replaceSpaces)

const maybeTitle = Maybe('Novo Post No Blog')
slug(maybeTitle) // Maybe {value: 'novo post no blog'}

const maybeNull = Maybe(null)
slug(maybeNull) // Maybe {value: null}

Feito isso, conseguimos compor funções de forma funcional e nos proteger do null.

Lembrando que precisamos fazer tudo isso, porque o JavaScript não é uma linguagem 100% funcional. Elm, por exemplo, já vem com tudo o que fizemos neste artigo construído na própria linguagem. Se quiser saber mais sobre Elm, sugiro ler este texto aqui. Tem alguma dúvida ou alguma contribuição? Aproveite os campos abaixo!

***

Artigo publicado originalmente em http://www.concretesolutions.com.br/2016/09/28/monads-em-javascript/.