DEV Community

Cover image for Um paralelo entre Programação Declarativa e Programação Imperativa
Maurício Witter
Maurício Witter

Posted on

Um paralelo entre Programação Declarativa e Programação Imperativa

Quais são os trade-offs entre o Paradigma Imperativo e o Paradigma Declarativo, princípios e uso


Introdução

As linguagens de programação podem ser classificadas de diversas formas, podem ser classificadas por Type System forte ou fraco; pelo nível de abstração (Machine code, Low level Assembler ou High level); se são compiladas ou interpretadas, ou por Paradigmas (Orientação a Objetos, Funcional, Procedural, Lógico, Imperativo, Declarativo entre outros). Neste artigo falaremos sobre o Paradigma Imperativo e Declarativo.

Entender o que são os paradigmas de programação, suas características vai ajudar você a entender melhor outros paradigmas como o Paradigma Funcional, Lógico, ou Orientado a Objetos, pois todos eles tem alguma similaridade com os paradigmas declarativo, declarativo ou ambos.

O Paradigma Imperativo foi o primeiro estilo de codificação que surgiu, com as primeiras linguagens de estudo short code. Já o estilo declarativo começou com a linguagem LISP (1958), com o Prolog (1972), e funcionais como o Haskell (1990).

No lado das linguagens imperativas temos, por exemplo, FORTRAN, ALGOL, Pascal, Ada e C entre outras, já no lado da linguagens declarativas temos, por exemplo: HTML, CSS, YAML e SQL. As linguagens JavaScript, Java, PHP e Rust, por exemplo, são multiparadigma e o estilo depende de como você escreve, mas nenhuma delas é puramente declarativa.

Paradigma Imperativo

O estilo imperativo concentra-se em como executar o código detalhe a detalhe e define o fluxo de controle de como instruções alteram o estado de um programa. Para ficar mais claro, o código imperativo ordena comandos ao computador, diz exatamente o que precisa fazer, como precisa fazer e em que ordem exata precisa executar.

A exemplo, o código abaixo é um algoritmo de ordenação chamado de bubble Sort que percorre o array n^2 fazendo a troca dos elementos até ordená-lo completamente. Nesse código há a construção da lógica e fluxo de controle, além da mutabilidade do array.

bubble sort

O código imperativo é interessante para programação de código de baixo nível para o qual existem poucas ou nenhuma abstração. O código fica extremamente verboso, ruim para depurar, ler e para dar manutenção. Em um código imperativo há muitos loops, condicionais e mutações de estado que facilmente resultam em bugs.

Assim sendo, a programação imperativa é mais adequada para tarefas onde é importante controlar o estado do programa e gerenciar o fluxo de execução.

Vamos calcular o fatorial de um número natural positivo, cuja fórmula está na imagem abaixo.

fatorial

No fatorial imperativo, avaliamos se n é menor ou igual a zero e retornamos 1, do contrário fazemos um loop até n-1 e multiplicamos o valor de n pelo índice fazendo a mutabilidade de n, ao final retornamos n. Nesse código criamos a lógica de calcular o fatorial de n e o fluxo de controle.

fatorial imperativo

Características do código Imperativo

  • Execução sequencial;
  • Estado mutável;
  • Gerenciamento manual de memória;
  • Não-determinístico.

Vantagens

  • Domínio explícito sobre o fluxo de controle;
  • Eficiência em uso de memória (se utilizado corretamente);

Desvantagens

  • Código verboso;
  • Mutabilidade de estado;
  • Efeitos colaterais.

Casos de uso

  • Sistemas de baixo nível: programação de sistemas de baixo nível, como drivers de dispositivo, sistemas operacionais e firmware. Essas tarefas requerem manipulação direta de locais de memória e registros de hardware, sendo melhor realizado usando um estilo imperativo.
  • Jogos e simulações: jogos e simulações geralmente envolvem algoritmos complexos e atualizações em tempo real, sendo mais fáceis de expressar usando um estilo imperativo. A programação imperativa fornece controle explícito sobre o fluxo do programa, tornando mais fácil escrever mecanismos de jogo eficientes e simulações físicas.
  • Operações de E/S: a programação imperativa é adequada para tarefas que envolvem muitos estados mutáveis ​​e efeitos colaterais, como operações de E/S. Programas imperativos podem manipular diretamente locais de memória e registros de dispositivos, tornando-os adequados para essas tarefas.

Paradigma Declarativo

O Paradigma declarativo, por outro lado, é um modelo de abstração de alto nível. Nesse estilo, declara-se o que deve ser computado ao invés de como deve ser computado, delegamos a implementação de controle para a linguagem.

Assim, a programação declarativa é como escrever um mapa detalhado para chegar a um destino. Você define o objetivo e descreve as etapas necessárias para alcançá-lo, permitindo que o computador determine a melhor rota e execute as ações correspondentes, sem precisar se preocupar com os detalhes de como cada passo é realizado.

Muitas linguagens que aplicam esse estilo tentam eliminar os efeitos colaterais descrevendo o que o programa deve realizar em termos do domínio do problema, em vez de descrever como realizá-lo como uma sequência das primitivas da linguagem de programação.

Conforme R. Kowalski em Communications of the ACM, Volume 22, Issue 7, Julho 1979, um algoritmo é composto pela lógica e pelo fluxo de controle. Na programação declarativa, a linguagem tende a abstrair a fluxo de controle e deixar para o programador desenvolver a lógica.

algorithm

Lloyd, JW. no artigo Practical advantages of declarative programming, explica que a programação declarativa pode ser entendida como “Forte” e “Fraca”. Isto é, no sentido “Forte”, o programador só fornece a lógica do que precisa e no sentido “Fraco”, o programador precisa fornecer a lógica e estender o fluxo de controle para computar o resultado esperado.

Princípios da Programação Declarativa

Para alcançar o estilo declarativo de programação, é necessário que alguns conceitos sejam aplicados. Os principais princípios deste paradigma são: Stateless, Transparência referencial, Abstração e Ausência de efeitos colaterais.

pilares da programação declarativa

Stateless

Em uma linguagem onde todos os dados são imutáveis, aplica-se o conceito de stateless. Isso significa que, uma vez que uma variável é atribuída, seu valor não pode ser alterado. Em vez disso, qualquer manipulação ou transformação de dados envolve o retorno de uma cópia dos valores originais transformados, de forma que os valores originais sem mantenham intactos. Em outras palavras, não fazemos a persistência dos dados entre as chamadas de execuções. Em JavaScript, funções como map, filter e flatMapsão stateless e funções como sort são stateful.

Uma função sem estado é aquela que não depende ou manipula o estado de contexto mais amplo. Ele pega tudo o que precisa como parâmetro e retorna um resultado, sem efeitos colaterais e nada armazenado entre as chamadas de execuções.

Conforme Nick Samoylov em Introduction to Programming, as operações sem estado geralmente não representam um problema ao alternar de um fluxo sequencial para um paralelo. Cada elemento é processado de forma independente e o stream pode ser dividido em qualquer número de substreams para processamento independente. Ou seja, o stateless acaba com bugs e race conditions em ambientes multithreading, não há razões para usar locks para sincronizar eventos dependentes.

Transparência Referencial

O valor de uma aplicação de função independe do contexto em que ocorre, isto é, dado um input a função deve retornar sempre o mesmo resultado. Por isso, a função deve ser independente do contexto, de forma que não use variáveis ou estados globais.

A função add não é referencialmente transparente pois dado o mesmo argumento, ela produz resultados diferentes:

let total = 0

const add = a => total += a

const r1  = add(1) // 1
const r2  = add(1) // 2
Enter fullscreen mode Exit fullscreen mode

Uma função ou expressão é referencialmente transparente se puder ser substituída por seu valor sem alterar o comportamento do programa.

Abstração

A abstração é um dos principais senão o principal princípio do Paradigma Declarativo, é a primeira coisa que vem a mente quando pensamos nela. Como já foi dito, dizemos o que queremos e não como queremos, assim delegamos a responsabilidade do que fazer para a linguagem de programação e apenas descrevemos como queremos o resultado a partir de determinada entrada.

Existem linguagens puramente declarativas, linguagens hibridas e multiparadigmas. YAML, SQL, HTML e XML são exemplos de linguagens puramente declarativas. Linguagens puramente funcionais ou lógicas como Haskell e Prolog, respectivamente, são hibridas pois apesar de ter seus próprios paradigmas (funcional e lógico) enfatizam a programação declarativa. Já as linguagens como Javascript, Java, PHP são multiparadigma, elas incluem algumas abstrações declarativas mas também incluem instruções imperativas e não opinam sobre qual meio usar.

Ausência de efeitos colaterais

Efeitos colaterais são alterações de estado ou execução de funções que não estão diretamente ligadas com a produção do resultado esperado de uma função. O Paradigma declarativo tem como objetivo eliminar as mudanças de estado em variáveis globais ou objetos compartilhados. Em vez disso, ela se concentra em avaliar expressões e produzir resultados imutáveis, o que ajuda a evitar problemas relacionados a estado e concorrência.

A função doSomething é considerada referencialmente transparente porque sempre retorna o mesmo valor de saída para uma determinada entrada, e não depende de nenhum estado externo. No entanto, apesar de ser referencialmente transparente, a função possui um side effect, ou seja, uma ação colateral que ocorre além da simples computação do valor de retorno, o side effect não faz parte da computação do valor de retorno da função, mas pode ter efeitos observáveis fora do escopo da função.

const doSomething = (arg) => {
  console.log("something");
  return arg;
}
Enter fullscreen mode Exit fullscreen mode

Características do código Declarativo

  • Stateless (estado interno não é relembrado entre chamadas);
  • Determinístico (as mesmas chamadas produzem os mesmos resultados);
  • Paralelismo (execução de múltiplas tarefas simultaneamente);
  • Transparência Referencial (uma expressão pode ser substituída por seu valor sem afetar o comportamento);
  • Modelo semântico claro e conciso;
  • Abstrações de alto nível;
  • Independência.

Vantagens

  • Legibilidade;
  • Concisão;
  • Recuperação de erros;
  • Reutilização;
  • Comutatividade;
  • Sem efeitos colaterais;
  • Idempotência.

Desvantagens

  • Baixo nível de controle;
  • Programação com estado (Stateful).

Casos de uso

  • Desenvolvimento Web: a programação declarativa é frequentemente usada no desenvolvimento Web, onde HTML, CSS e outras linguagens de marcação são usadas para definir a estrutura e a aparência das páginas da Web. A programação declarativa permite que os desenvolvedores especifiquem a aparência da página, em vez de como obter essa aparência.
  • Processamento de dados: a programação declarativa é adequada para tarefas de processamento de dados, como consultar bancos de dados e manipular grandes conjuntos de dados. Linguagens de programação declarativas como SQL fornecem uma interface de alto nível para interagir com dados, tornando mais fácil escrever consultas e transformações complexas.
  • Programação funcional: a programação funcional enfatiza o uso de funções puras e estruturas de dados imutáveis. A programação funcional é adequada para tarefas que exigem muita simultaneidade ou paralelismo, pois funções puras não têm efeitos colaterais e podem ser executadas em paralelo com segurança.

Conclusão

Muitas das linguagens imperativas começaram a trazer novos recursos na última década para o estilo declarativo. Um exemplo é o Java 8 que trouce uma nova API para Streams para operar em coleções usando expressões lambda abstraindo o código imperativo.

No Front-end também ganhamos muito mais produtividade e segurança com os frameworks declarativos que surgiram nos últimos anos, como o React, o Vue, o Angular, o Next, o Nuxt entre outros.

Na área de cloud-computing, também existem várias ferramentas no estilo declarativo como o Terraform, Ansible, Docker, Kubernetes que auxiliam a colocar aplicações em produção com escalabilidade, segurança e confiabilidade, fazendo o CI/CD.

A crescente popularidade das linguagens de programação declarativas decorre da simplicidade inerente do paradigma, cujo alto nível de abstração garante estilos de programação legíveis para humanos. — MOTTOLA A.. Design and implementation of a declarative programming language in a reactive environment. 2005.

Se você chegou até aqui, espero que tenha tido uma ótima leitura e que esse conteúdo possa ter lhe ajudado de alguma forma. Obrigado por ler! Qualquer dúvida, agregação ou correção, envie-me uma dm no twitter .

Referências

Imagem: nordicapis

Top comments (0)