Paradigma e linguagens funcionais: uma visão geral

Paradigma e linguagens funcionais: uma visão geral

Introdução

Linguagens imperativas são comumente baseadas na arquitetura de von Neumann. Em 1977, o renomado cientista da computação John Backus discursou no Prêmio Turing da ACM, defendendo as vantagens do paradigma funcional sobre o imperativo. Ele argumentou que linguagens funcionais são mais legíveis, confiáveis e propensas a serem corretas. Essas linguagens aspiram a emular funções matemáticas, tornando seus parâmetros e variáveis em grande parte imutáveis.

Embora tenham sido feitas muitas tentativas de demonstrar a superioridade das linguagens funcionais, nem todas foram bem-sucedidas. No entanto, com o amadurecimento dessas linguagens, seu interesse e uso têm aumentado significativamente.

Estado e Imutabilidade

Um desafio nas linguagens imperativas é o gerenciamento do estado mutável das variáveis. Em contrapartida, linguagens funcionais geralmente evitam essa complicação. Lisp, a primeira linguagem de programação funcional, ainda é amplamente utilizada e desempenha um papel crucial em áreas como representação de conhecimento, aprendizado de máquina e modelagem de fala. Ao longo dos anos, surgiram diversos dialetos de Lisp, incluindo Scheme.

Funções Matemáticas e Computação

No âmbito das funções matemáticas, estas representam um mapeamento de elementos de um conjunto domínio para um conjunto imagem. Um aspecto crucial é que a ordem de avaliação das expressões de mapeamento é controlada de forma recursiva e com expressões condicionais. Alonzo Church introduziu um método para definir funções não nomeadas, conhecidas como expressões lambda, que se tornaram a base do cálculo lambda. Este pode ser tanto tipado quanto não-tipado.

Formas Funcionais

Uma forma funcional é uma função que recebe uma ou mais funções como parâmetros, retorna uma função ou faz ambos. Um exemplo comum em matemática pura é a composição de funções, como em z(x) = f(g(h(x))). A forma funcional apply-to-all aplica uma função a todos os elementos de uma lista e armazena os resultados em uma nova lista.

Características Principais

Linguagens de programação funcionais são fundamentadas na ausência de estado mutável (em linguagens puramente funcionais), na transparência referencial e na aplicação de funções como operações básicas. Embora existam linguagens puramente funcionais, muitas incorporam conceitos imperativos.

Estruturas de Dados em Lisp

Na versão original da linguagem Lisp, existiam apenas duas categorias de objetos de dados: átomos e listas. As listas são armazenadas como listas encadeadas, contendo dois ponteiros: um para o primeiro elemento (que pode ser uma lista ou um átomo) e outro para o próximo elemento da lista. O último elemento aponta para nil.

Evolução e Recursividade

John McCarthy, o pioneiro por trás do Lisp, inicialmente promoveu o processamento de listas como uma abordagem para o processamento simbólico em geral. Ele desenvolveu a função eval, capaz de avaliar qualquer outra função em Lisp. Essa inovação permanece como um marco no desenvolvimento do Lisp. Outra característica dos sistemas Lisp iniciais foi o uso de escopo dinâmico, onde as funções são avaliadas no ambiente de suas chamadas. Curiosamente, um interpretador Lisp pode ser escrito em Lisp, ilustrando a natureza auto-referencial e a semântica operacional da linguagem.


Scheme

O dialeto de Lisp conhecido como Scheme foi desenvolvido na década de 1970. Caracteriza-se por sua simplicidade, escopo estático e o tratamento de funções como entidades de primeira classe. O interpretador de Scheme, em modo interativo, opera em um ciclo de leitura-avaliação-impressão. Neste ciclo, as expressões são interpretadas pela função EVAL, e os literais são avaliados como eles próprios.

Funções Aritméticas e Operações

Scheme oferece funções primitivas para operações aritméticas básicas, como +, -, * e /. As funções * e + podem receber zero ou mais argumentos. Quando não fornecidos argumentos, * retorna 1 e + retorna 0. A função + soma todos os argumentos fornecidos, enquanto * os multiplica. / divide o primeiro argumento pelo restante e - subtrai todos os argumentos (exceto o primeiro) do primeiro.

Definição de Funções e Variáveis

Um programa em Scheme é composto por definições de funções. Funções anônimas são definidas usando a palavra-chave LAMBDA, como em (LAMBDA (x) (+ x x)). A diretiva DEFINE serve tanto para criar um nome para um valor, funcionando como uma constante, quanto para nomear uma função lambda.

I/O e Pureza Funcional

Embora Scheme possua operações de entrada e saída, isso o torna uma linguagem funcional "impura". A linguagem também oferece várias funções de predicado numérico e condicional.

Manipulação de Listas

Scheme herda do Lisp tradicional suas capacidades robustas para manipulação de listas, que incluem:

  • QUOTE: Retorna o argumento como fornecido.
  • CAR e CDR: Desconstroem uma lista.
  • CONS: Constrói uma nova lista a partir de duas partes de uma lista existente.
  • LIST: Constrói uma lista a partir de um número arbitrário de argumentos, semelhante a encadear várias chamadas de CONS.

Além disso, as funções de predicado EQ?, NULL? e LIST? realizam verificações sobre os argumentos fornecidos. A função LET permite a definição de variáveis locais que saem de escopo ao final de sua execução.

Recursão em Cauda

Um aspecto notável de Scheme é o suporte para recursão em cauda, em que a chamada recursiva é a última operação da função. Isso permite que o compilador otimize a recursão para usar iteração, tornando-a mais eficiente.

Composição Funcional e Metaprogramação

A única forma funcional primitiva fornecida pelo Lisp original é a composição funcional, que permite o aninhamento de funções. Este conceito é herdado por Scheme. A função MAP é um exemplo da forma funcional que aplica uma função a todos os elementos de uma lista.

Devido à sua simplicidade, em Scheme é possível criar funções que geram código em tempo de execução. A função EVAL permite que códigos sejam executados com base no estado atual do programa.


Common Lisp

Common Lisp é uma linguagem de programação multifacetada que surgiu do esforço de consolidar diversos dialetos de Lisp, embora não tenha sido diretamente derivada de Scheme. Com um vasto conjunto de recursos, ela pode ser complexa, mas também é incrivelmente poderosa. Sua sintaxe é uma evolução daquela encontrada no Lisp original.

Função Fatorial

A função fatorial em Common Lisp é comumente definida da seguinte forma:

(DEFUN factorial (n)
  (IF (<= n 1)
      1
      (* n (factorial (- n 1)))))

Note que o código original tinha alguns erros, como a variável x que deveria ser n, e o operador de multiplicação que estava escrito como \* em vez de *.

Recursos e Estruturas de Dados

Common Lisp oferece uma gama rica de estruturas de dados e recursos, incluindo registros, matrizes, números complexos e strings. Também fornece capacidades de E/S (entrada e saída) eficientes. Devido à sua natureza como uma "união" de vários dialetos de Lisp, ela suporta tanto programação funcional quanto imperativa, e inclui alguns tipos de dados mutáveis.

Escopo

A linguagem oferece flexibilidade para trabalhar com escopo tanto estático quanto dinâmico. Em casos específicos onde o escopo dinâmico é desejado, ele deve ser explicitamente declarado.

Macros

Um dos recursos mais poderosos de Common Lisp é o sistema de macros, que permite a metaprogramação. Algumas implementações de macros são fornecidas nativamente pela linguagem.

Símbolos

Assim como outras variantes de Lisp, Common Lisp utiliza símbolos como um tipo de dado. Palavras reservadas, como T (verdadeiro) e NIL (falso ou vazio), são símbolos que se avaliam a si mesmos. Os símbolos podem ser "vinculados" ou "desvinculados", dependendo do contexto. Símbolos parametrizados são temporariamente vinculados durante a avaliação de uma função, enquanto símbolos que atuam como nomes de variáveis são considerados vinculados. Outros símbolos, como A, B e C, são normalmente desvinculados a menos que sejam explicitamente vinculados a um valor.


ML (Meta Language)

ML é uma linguagem de programação funcional que, como Scheme, também usa escopo estático. No entanto, é significativamente diferente das linguagens baseadas em Lisp, como Scheme, principalmente porque é fortemente tipada. Em contraste, Scheme tem um sistema de tipos mais permissivo.

Variáveis e Ambiente de Avaliação

Embora ML use o conceito de imutabilidade, onde os valores uma vez definidos não podem ser alterados, é impreciso dizer que ela "não adota variáveis". Em ML, identificadores são usados para se referir a valores, e uma vez ligados a um valor, não podem ser reatribuídos. O ambiente de avaliação em ML armazena todos os identificadores declarados, juntamente com seus tipos, facilitando o processo de inferência de tipos.

Sintaxe e Funções

A sintaxe de ML se assemelha mais às linguagens imperativas, especialmente na forma como as funções são declaradas. Uma função em ML segue o padrão:

fun nome_funcao(parametros) = expressao;

Quando uma função é chamada, o valor da expressão é retornado. Esse valor pode ser uma única expressão ou uma lista de expressões, separadas por vírgulas ou dentro de parênteses.

Tipos e Inferência

ML usa inferência de tipos para determinar automaticamente os tipos de variáveis e expressões. Em operações numéricas, o tipo de retorno é inferido com base nos tipos dos operandos. Se um literal de ponto flutuante é usado em uma expressão, o resultado também será de ponto flutuante.

Fluxo de Controle e Pattern Matching

ML adota estruturas de controle como if-then-else e também suporta pattern matching, o que permite a decomposição de dados complexos de maneira elegante.

Funções de Ordem Superior

ML oferece funções de ordem superior como map e filter. É importante observar que em ML, todas as funções são, em teoria, funções que aceitam apenas um argumento. Para funções que "recebem" múltiplos argumentos, ML utiliza uma técnica conhecida como "currying", que transforma uma função de múltiplos argumentos em uma série de funções que tomam um único argumento.


Haskell

Haskell é uma linguagem de programação funcional que compartilha algumas semelhanças com ML, como o uso de escopo estático, forte tipagem e inferência de tipos. No entanto, elas têm diferenças fundamentais. Uma das principais é que Haskell é uma linguagem puramente funcional, o que significa que ela evita efeitos colaterais. Além disso, Haskell suporta polimorfismo paramétrico, permitindo um maior grau de abstração.

Sobrecarga de Funções

Em contraste com ML, Haskell permite a sobrecarga de funções através de classes de tipos, mas isso não significa que uma função pode ser "redefinida" mais tarde no código. Em vez disso, uma função pode ser definida para operar em múltiplos tipos de dados, contanto que esses tipos sejam membros da classe de tipos apropriada.

Tipos de Dados

Haskell oferece uma ampla variedade de tipos de dados, incluindo inteiros, números de ponto flutuante, caracteres e listas. As listas em Haskell são particularmente poderosas e podem ser manipuladas por meio de uma série de funções de lista predefinidas.

Pattern Matching

Assim como ML, Haskell também suporta pattern matching, uma característica que torna o código mais legível e expressivo, permitindo decomposição de estruturas de dados de forma elegante.

Avaliação Preguiçosa

Diferentemente de linguagens de programação estritas, Haskell utiliza avaliação preguiçosa. Isso significa que os argumentos de uma função não são avaliados até que sejam realmente necessários. Essa característica permite eficiências computacionais e a criação de estruturas de dados infinitas, entre outras vantagens.


FSharp

F# é uma linguagem de programação funcional destinada à plataforma .NET. Embora seja inspirada em OCaml e compartilhe semelhanças com ML, não é correto afirmar que ela é uma descendente de Haskell. É uma linguagem multi-paradigma que, além do paradigma funcional, também suporta programação imperativa e orientada a objetos. Como uma linguagem de "primeira classe" no ecossistema .NET, F# pode interoperar facilmente com outras linguagens suportadas pela plataforma, como C#.

Tipos de Dados

A linguagem oferece uma ampla gama de tipos de dados, incluindo tuplas, listas, uniões discriminadas, registros e variáveis tanto mutáveis quanto imutáveis.

Avaliação Preguiçosa e Sequências

F# também incorpora o conceito de avaliação preguiçosa em sua implementação de sequências, semelhante à abordagem de Haskell. Os elementos de uma sequência são calculados somente quando necessários, o que pode ser demonstrado pelo seguinte exemplo:

let numeros = seq {2..7..17}
for num in numeros do printfn "valor = %d" num

Isso imprimirá os números 2, 9 e 16. Embora sequências sejam avaliadas de forma preguiçosa, listas e vetores em F# não são, e seus elementos são armazenados na memória de forma imediata.

Funções

As funções em F# têm uma sintaxe similar à de ML e Haskell. Se uma função é nomeada, é precedida pela palavra-chave let. Funções anônimas são definidas usando a palavra-chave fun. A linguagem não realiza coerção automática de tipos, então uma operação entre tipos incompatíveis resultará em um erro.

Currying

O suporte ao "currying" está presente em F#. Isso significa que todas as funções com mais de um parâmetro podem ser parcialmente aplicadas, tornando-se funções de um único parâmetro que retornam uma nova função.


Programação Funcional em Linguagens Imperativas

Embora tradicionalmente voltadas para o paradigma imperativo, muitas linguagens de programação modernas incorporaram características funcionais para beneficiar-se da expressividade e eficiência que esse paradigma oferece. Por exemplo, linguagens como JavaScript, Python, Ruby, Java e C# agora suportam funções anônimas, comumente chamadas de "lambdas".

Uso de Funções Lambda

Funções lambda são frequentemente usadas em métodos que aceitam funções como argumentos. Em JavaScript, o método .map é um exemplo clássico disso. C# possui o método FindAll, que retorna todos os elementos de uma coleção que satisfazem uma determinada condição. Em C#, lambdas também podem ser atribuídas a variáveis e, assim, serem nomeadas.

Em Python, além das funções map e filter, há suporte para funções parciais, permitindo que uma variável armazene uma função que executa cálculos com alguns argumentos predefinidos. Ruby suporta blocos de código que podem ser convertidos em objetos de primeira classe através de lambdas.

Comparação Entre Paradigmas

Linguagens funcionais como Lisp são muitas vezes mais simples em sua construção, usando uma estrutura unificada de listas para representar tanto dados quanto código. Isso contrasta com a complexidade sintática de muitas linguagens imperativas.

Em termos de semântica, linguagens funcionais frequentemente minimizam ou eliminam efeitos colaterais, enquanto linguagens como C e suas derivadas incluem efeitos colaterais como uma parte intrínseca de várias expressões.

Eficiência e Tamanho do Código

Embora programas escritos em linguagens funcionais tendam a ser mais curtos, isso não implica necessariamente em menor complexidade. Além disso, é um equívoco generalizar que linguagens funcionais interpretadas são mais lentas do que linguagens imperativas compiladas. Com a evolução dos compiladores para linguagens funcionais, a diferença de desempenho tem diminuído significativamente.

Legibilidade e Concorrência

A legibilidade pode ser subjetiva e depende do problema em questão. No entanto, é válido dizer que programação funcional muitas vezes simplifica tarefas complexas como concorrência e paralelismo, graças à imutabilidade e à ausência de estados globais, que são características intrínsecas desse paradigma.

Este artigo foi originalmente escrito em 2021 para a disciplina de Linguagens de Programação, e revisado em setembro de 2023.

Did you find this article valuable?

Support Gustavo Becelli by becoming a sponsor. Any amount is appreciated!