Neste artigo iremos introduzir os principais conceitos de Programação Funcional, conhecendo os seus princípios e desvendando o seu poder através de alguns exemplos práticos na linguagem Clojure.
Motivação
Com o passar dos anos, novas necessidades foram surgindo em âmbitos específicos da computação, tais como: “o desenvolvimento da Inteligência Artificial e seus subcampos – computação simbólica, prova de teoremas, sistemas baseados em regras e processamento de linguagem natural”. O paradigma predominante nas linguagens de programação da época era o imperativo – que não atendia bem a essas necessidades existentes. Desse modo, tornou-se necessário a existência de um novo modelo de programação capaz de propiciar recursos eficientes e elegantes para suprir as carências computacionais que gradativamente surgiam.
Sendo assim, no início da década de 60 o paradigma funcional nasceu – originalmente na linguagem Lisp, trazendo algumas propostas substancialmente interessantes para resolver as questões que outras linguagens de programação com paradigmas distintos não conseguiam atender.
O Paradigma Funcional: Princípios
Na programação funcional a modelagem de um problema computacional é composta por uma coleção de funções que interagem entre si utilizando recursos como: composição funcional, condições e recursão. Essencialmente, “a computação é vista como uma função matemática mapeando entradas e saídas” – onde não há uma noção de estado, e com essa perspectiva, distingue-se diretamente dos paradigmas imperativo, lógico e/ou orientado à objeto.
Nos tópicos a seguir, conheceremos os seus principais princípios.
Imutabilidade
Assim como na matemática, as linguagens funcionais possuem a característica de tratar variáveis como sendo expressões reais e imutáveis, em que não há um conceito de célula de memória e portanto, alteração de referência de memória. Essa característica traduz, porém, uma linguagem funcional pura, em que variáveis são usadas para nomear expressões imutáveis e eliminam o operador de atribuição; do contrário, são consideradas impuras.
Observe o código a seguir:
x = x + 1
Neste trecho de código, em uma linguagem imperativa ou mesmo orientada a objeto, estamos realizando uma instrução de atribuição, o que significa: alterar o estado do programa, somando o valor 1 à variável x e armazenando o resultado desta soma na mesma variável que representa uma referência de memória denominada x.
Observe abaixo um exemplo em que conseguimos realizar o mesmo comportamento do código anterior, mas desta vez sem alterar estado e célula de memória.
(def x (+ 1))
;; => x
;; => 1
;; ( ) marca o início e fim de uma instrução
;; def é uma palavra chave para criarmos símbolos
;; + é uma função de adição (soma)
Desta forma, os nossos programas utilizam composição de funções ao invés de atualizar variáveis em memória. Como benefício, a imutabilidade nos permite conhecer mais facilmente o estágio atual da aplicação – já que cada valor referenciado no programa será sempre o mesmo de quando ele foi originalmente criado. Desse modo, lidar com questões como concorrência torna-se mais simples, pois uma vez que eliminamos a noção de estado, diminuímos a necessidade de gerenciar atualizações simultâneas e utilizar travas (locks) nos valores.
Transparência Referencial
Quando o valor de uma função depende exclusivamente do valor de seus argumentos, em linguagens funcionais, essa propriedade é conhecida como transparência referencial:
(defn sum
([x y] (+ x y)))
;; => (sum 4 6)
;; => 10
;; defn é uma palavra chave para definirmos uma função
;; sum é o nome da função
;; [x y] são a lista de parâmetros
No exemplo acima, criamos uma função chamada sum – que soma dois números. O valor do seu resultado depende unicamente do valor de seus argumentos e não necessita de alguma computação prévia ou mesmo da ordem de avaliação de seus argumentos.
Dessa forma, as funções expressam diretamente a pureza que devem ter, não alterando estado e sempre retornando o mesmo valor independente do contexto.
(defn upper-s
([msg] (clojure.string/upper-case msg)))
;; => (upper-s “Hello World!”)
;; => “HELLO WORLD!”
;; upper-case é uma função que converte string em maiúsculas
Funções: Cidadãs de Primeira Classe
Outra particularidade das liguagens funcionais, é que funções são tratadas como qualquer outro elemento da linguagem, por exemplo: strings, booleanos, inteiros, etc. Essa característica é conhecida como first-class functions, em que as funções podem ser atribuídas a variáveis, passadas para outras funções, retornadas por outras funções e até mesmo criadas em runtime:
(def messenger
(defn sender
([] (sender “Hey Jude!”))
([msg] (println msg)))
;; => (messenger)
;; => Hey Jude!
;; => (messenger “Dear Mary!”)
;; => Dear Mary!
Neste exemplo, criamos uma função chamada sender e atribuímos o seu resultado a um símbolo que chamamos de messenger. Na função sender, declaramos duas aridades (número de argumentos/operandos que uma função pode ter), uma sem parâmetro e a outra com um parâmetro denominado msg. A primeira aridade, invoca a segunda com um valor default a ser impresso – resultante da invocação recursiva da função sender. Desse modo, é possível perceber que messenger pode ser invocada passando o número apropriado de argumentos (que pode ser nenhum ou um).
Conclusão
Programar no modelo funcional traz uma mudança de perspectiva que adquirimos e nos acostumamos ao longo do tempo, programando em linguagens de paradigmas diferentes. Entretanto, a medida que os princípios funcionais vão se consolidando, começamos a compreender e experimentar os benefícios que este modelo realmente proporciona.