Os conceitos básicos de programação funcional são simples e me tornaram um programador melhor: código com menos bugs, testes unitários triviais, código mais claro e maior produtividade. Há um tempo eu fiz uma talk interna na Agilize sobre isto, mas ainda vejo muita gente nas comunidades que não usa. Espero que esse texto melhore sua forma de programar também.

O que é programação funcional?

Programação funcional é um paradigma de programação que trata a computação como uma avaliação de funções matemáticas e que evita estados ou dados mutáveis.

Ou seja, você programará conferindo características de funções matemáticas às funções do seu código.

Benefícios da programação funcional

  • Mais código com menos bug, pois reduz os efeitos colaterais;
  • Facilita testes unitátios, pois o comportamento depende só das entradas;
  • Código mais fácil de manter, pois inputs e outputs são explícitos;
  • Mais código em menos tempo, pois facilita reuso e composição.

Como adotar programação funcional?

Você pode adotar agora mesmo, na sua próxima função. Não é necessário migrar seu código todo para o paradigma funcional. E nem mesmo usar só com linguagens puramente funcionais. Se a linguagem que você usa tem funções, você pode usar programação funcional e combinar com outros paradigmas, como OO.

Mas antes de ver os exemplos, vamos alinhar nossa definição de função.

Função segundo a matemática

Na matemática, função é uma relação de um conjunto A com um conjunto B. Dado x um elemento de A e f(x) = y um elemento de B, a relação f é função se atender à duas características especiais:

  1. y depende única e exclusivamente de x;
  2. f associa x a um único valor de y.

Se a partir de um mesmo x eu chego a dois elementos distintos de B, esta relação não é uma função.

drawing
Fig.1 Duas relações f: (i) é função, mas (ii) não é função, pois f associa x a mais de um elemento de B.

Função segundo a computação

Na computação, uma função é um bloco de código que executa uma tarefa e retorna um resultado—implicita ou explicitamente. Você pode reusar este mesmo bloco várias vezes no seu código.

Função pura segundo a computação

Ainda na computação, uma função pura é uma função que tem as seguintes características:

  1. Sem inputs ou outputs ocultos—transparência referencial;
  2. Mesmo parâmetro, mesmo resultado sempre—idempotência.

Note então que uma função pura na computação é basicamente uma função segundo a matemática!

Transparência referencial

Sem inputs ou outputs ocultos.

Vamos ver os problemas existentes na classe Calendar a seguir. Eles são pequenos, mas o suficiente para causar estragos e se aplica à várias linguagens:

<?php
class Calendar {
    public $defaultInterval = "P1D";
    
    // ...

    function addOneDay($date) {
        return $date->add(new DateInterval($this->defaultInterval));
    }
}

Quais são os problemas dessa classe?

Inputs ocultos

O método addOneDay usa internamente defaultInterval para definir o intervalo. Mas o valor de defaultInterval pode ser modificado externamente sem você perceber e o método retornará um valor insperado:

<?php
$calendar = new Calendar();
$calendar->defaultInterval = 'P2D'; // redefiniram, e você não viu

$now = new DateTime;
// $now --> 2018-12-15

$nowAddedInOneDay = $calendar->addOneDay($now);
// $nowAddedInOneDay --> 2018-12-17 (esperado: 2018-12-16)

Algumas formas de resolver: trazer a defaultInterval (que é uma ISO 8601 duration specification) como string para dentro de addOneDay ou transforma-la em uma constante. Assim, você garante que somente $date modificará o resultado do método:

<?php
class Calendar {
    // ...
    
    function addOneDay($date) {
        return $date->add(new DateInterval("P1D"));
    }
}

Outputs ocultos

No PHP, assim como em várias outras linguagens, objetos são passados por referência como argumentos de funções. Já vi muito programador mal informado introduzir bug assim:

<?php
$calendar = new Calendar();

$now = new DateTime;
// $now --> 2018-12-15

$nowAddedInOneDay = $calendar->addOneDay($now);
// $nowAddedInOneDay --> 2018-12-16
// $now --> 2018-12-16 (foi passado por referência e também foi atualizado!)

Uma forma de resolver: faça um clone do objeto $date dentro da função. Assim, você garante que o objeto externo não será modificado e remove um output oculto.

<?php
class Calendar {
    // ...
    
    function addOneDay($date) {
        $date = clone $date;
        return $date->add(new DateInterval("P1D"));
        // ou simplesmente:
        // return (clone $date)->add(new DateInterval("P1D"));
    }
}

Agora, o resultado de addOneDay depende exclusivamente de $date. E não modifica nenhuma outra coisa inadivertidademente.

Existem várias formas de resolver esses tipos de problemas. O importante é você garantir que não existam inputs nem outputs ocultos indesejados.

Idempotência

Mesmo parâmetro, mesmo resultado sempre.

Se sua função tem transparência referencial, ela é idempotente (conclusão minha, sem prova matemática).

Ou seja, se a função só depende dos argumentos para gerar o resultado e você passa os mesmos argumentos, o resultado tem que ser o mesmo. Se não for, existe algum input implítico. E, por tanto, a função não tem transparência referencial.

Exemplo bobo de uma função JavaScript que não é idempotente:

Math.random();
// 0.04912023550589373

Math.random();
// 0.9020578857167636

Math.random();
// 0.6453029357741913

Exemplo menos bobo: em JavaScript, já li que a função slice() é idempotente, mas a splice() não é idempotente. Eu discordo: ambas são idempontentes.

var arr = [1, 2, 3, 4, 5];

arr.slice(1,4);
// (3) [2, 3, 4]

arr.slice(1,4);
// (3) [2, 3, 4]

arr.slice(1,4);
// (3) [2, 3, 4]

Cada vez que eu executo slice(), ela retorna uma shallow-copy de parte de um array em um novo objeto array. Não importa quantas vezes eu chame slice() no mesmo array. O retorno é sempre igual.

Já a splice():

var arr = [1, 2, 3, 4];

arr.splice(0,2);
// (2) [1, 2]

arr.splice(0,2);
// (2) [3, 4]

arr.splice(0,2);
// []

Cada vez que eu chamo splice(), ela retorna um resultado diferente.

Mas isso acontece porque ela altera o conteúdo do array enquanto remove os elementos e os retorna. Na segunda chamada à função, o array já é outro. O array também é argumento da função, só que é implícito. Por isso o resultado mudou.

De fato splice() é idempotente. Só que ela não é pura porque não tem transparência referencial.

Conclusão

Minha sugestão? Use funções puras sempre que puder ou fizer sentido.

Ou seja, funções:

  1. Sem inputs ou outputs ocultos—transparência referencial;
  2. Nas quais dados os mesmos parâmetros, retornam o mesmo resultado sempre—idempotência.

Suas funções e métodos serão previsíveis, com inputs explícitos e sem efeitos colaterais. Isso lhe dará grande cofiança nos testes e legibilidade no código. Note que você pode usar esses conceitos junto com conceitos de Orientação a Objetos.

Mas esta é só a ponta do icerberg.

Espero que os conceitos deste texto lhe ajudem a usar funções puras em funções como filter(), map(), find(), reduce(), presentes em várias linguagens. E lhe ajudem também a explorar outras coisas, como monads, imutabilidade, recursão, polimorfismo paramétrico, currying, closures, functors, memoização, avaliação tardia etc.

Para fechar, selecionei umas frases do The Zen of Python:

  • Explicit is better than implicit.
  • Flat is better than nested.
  • If the implementation is hard to explain, it’s a bad idea.
  • If the implementation is easy to explain, it may be a good idea.

Você já pode melhorar agora a sua forma de programar.