Paradigma Imperativo x Declarativo x Funcional
Conceitos
Paradigma Imperativo
Estilo de programação em que você diz ao computador como fazer algo, passo a passo.
Foca em comandos, controle de fluxo (if, for, etc.) e mudança de estado em variáveis.
No imperativo, o raciocínio costuma ser mais “top down”: uma sequência de passos explícitos que o programa deve seguir.
No modelo imperativo, passo 1 → passo 2 → passo 3
Paradigma Declarativo
Estilo de programação em que você diz ao computador o que quer obter, sem detalhar o passo a passo.
Foca em descrever o resultado desejado, enquanto o “como fazer” fica escondido na linguagem, biblioteca ou framework.
No declarativo, o raciocínio costuma ser mais “por descrição de regras/expressões** (muitas vezes via funções ou expressões), dizendo o que deve ser obtido, e não o passo a passo.
Modo declarativo NEM SEMPRE é "por funções":
- SQL:
SELECT ... WHERE ... - HTML:
<button>,<div> - Regras em YAML, etc.
Exemplos:
- SQL:
SELECT * FROM pedidos WHERE valor > 100;
- HTML:
<button>Salvar</button>
Paradigma Funcional
Paradigma declarativo e paradigma funcional não são a mesma coisa.
Programação funcional é um tipo de programação declarativa, mas nem toda programação declarativa é funcional.
Paradigma funcional É um tipo de programação declarativa que segue algumas regras fortes
-
Funções puras
- Mesma entrada → sempre a mesma saída
- Não mexem em variáveis globais, não escrevem em banco, não printam na tela (idealmente).
-
Imutabilidade
- Você não altera estruturas de dados; cria novas versões.
- Sem “sair mutando” variáveis por aí.
-
Funções como valores
- Você passa função como parâmetro, retorna função, compõe função com função (
map,filter,reduce).
- Você passa função como parâmetro, retorna função, compõe função com função (
-
Pouco ou nenhum controle de fluxo explícito
- Quase nada de
for/while; no lugar,map,filter,reduce, recursão.
- Quase nada de
Exemplo funcional em JS:
const numeros = [1, 2, 3, 4, 5];
const pares = numeros.filter(n => n % 2 === 0);
const soma = pares.reduce((acc, n) => acc + n, 0);
Aqui você:
- Fala o que quer (pares, depois soma) → declarativo
- E faz isso só com funções, sem mudar estado externo → funcional
Como enxergar a relação do paradigma declarativo x funcional
Pensa assim:
-
Paradigma Declarativo = guarda-chuva maior
Tudo que foca em dizer o que você quer, e não como fazer. -
Declarativo = jeito de falar com o computador
-
Funcional = _um “estilo dentro do declarativo”, com regras extras
-
Declarativo: foca em descrever o resultado (“o que”) e esconde o passo a passo.
-
Funcional: é um jeito declarativo de programar onde:
- você organiza tudo em funções puras,
- evita estado mutável,
- e compõe resultados com
map/filter/reduceetc.
👉 Todo código funcional é (em essência) declarativo, mas dá pra ser declarativo sem ser funcional (SQL, HTML, etc.).
Dentro desse guarda-chuva, temos alguns estilos, por exemplo:
- Programação funcional (Haskell, grande parte de Clojure, Elm, etc.)
- Lógicas / Regras (Prolog, regras de engines)
- Consultas (SQL)
- Declaração de interface / layout (HTML, CSS)
Então:
- SQL é declarativo, mas não é funcional.
- HTML é declarativo, mas não é funcional.
- Haskell é funcional e também declarativo.
Programação declarativa é o estilo em que descrevemos o que queremos que o programa faça.
Programação funcional é um tipo de programação declarativa que se baseia em funções puras, imutabilidade e composição de funções.
Todo código funcional é declarativo, mas nem todo código declarativo é funcional.
Ponto Central
Imperativo: você descreve como fazer.
Declarativo: você descreve o que quer.
Funcional: é um tipo de paragima declarativo que segue algumas regras fortes: funções puras, imutabilidade, funções como valores, pouco ou nenhum controle de fluxo explícito.
| Paradigma | Foco principal | Como se pensa o código | Exemplos típicos |
|---|---|---|---|
| Imperativo | Como fazer (passo a passo) | Sequência de comandos, loops, if, variáveis |
C, Go, Java, JS “na mão” com for, if, atualizando estado |
| Declarativo | O que eu quero (resultado desejado) | Descrição de regras/consultas/estrutura | SQL, HTML, CSS, React JSX, ORMs com query em alto nível |
| Funcional | O que eu quero, via funções puras | Composição de funções, map, filter, reduce, imutabilidade |
Haskell, Clojure, Elm, partes funcionais de JS/TS (Ramda, etc.) |
Qual abordagem é melhor?
A melhor abordagem é aquela em que olhando o código você entende a intenção sem esforço.
-
Se o estilo declarativo deixa a regra cristalina (
Filter,Reduce, SQL, etc.), use declarativo. -
Se pra entender o que está acontecendo você precisa “abrir a cabeça” da função genérica, aí o declarativo virou mágica demais → melhor voltar para o imperativo simples.
-
Na prática do dia a dia, vamos notar que misturamos os dois.
👉 Regra prática que eu uso:
- Prefira declarativo até o ponto em que continua óbvio.
- Passou desse ponto, volte pro imperativo explícito.
Exemplo Prático
Paradigma Imperativo
- Foco: como chegar no resultado.
- Você controla o fluxo: laços, if, variáveis mudando de valor.
- É como dar receita de bolo: “pegue X, depois faça Y, depois Z…”.
- Controle total do passo a passo (ótimo para performance, otimizações finas, algoritmos complexos).
- Mais fácil de debugar em Go: você vê o
for, põe breakpoint, vê variáveis mudando. - Em Go, é o jeito idiomático padrão (a linguagem foi pensada com loops simples e claros).
- Tem tudo ali dentro: validação, regra de negócio, update.
- É claro, mas a lógica de domínio fica misturada com detalhe mecânico (
for,map, etc.).
- Exemplos de linguagens (no uso comum):
- C, Java, Go, JavaScript (quando você escreve loops,
if,for, etc.)
type ItemPedido struct {
ProdutoID int64
Qtd int
}
type Estoque struct {
ProdutoID int64
QtdAtual int
}
type MapaEstoque map[int64]int
// AtualizarEstoqueImperativo baixa o estoque diretamente a partir dos itens do pedido.
func AtualizarEstoqueImperativo(estoque MapaEstoque, itens []ItemPedido) error {
// 1. Valida e atualiza tudo num loop só
for _, item := range itens {
qtdAtual, ok := estoque[item.ProdutoID]
if !ok {
return fmt.Errorf("produto %d não encontrado no estoque", item.ProdutoID)
}
if item.Qtd <= 0 {
return fmt.Errorf("quantidade inválida para produto %d: %d", item.ProdutoID, item.Qtd)
}
if qtdAtual < item.Qtd {
return fmt.Errorf("estoque insuficiente para produto %d: atual=%d, solicitado=%d",
item.ProdutoID, qtdAtual, item.Qtd)
}
// 2. Atualiza o estoque
novo := qtdAtual - item.Qtd
estoque[item.ProdutoID] = novo
}
return nil
}
Paradigma Declarativo
Você diz o que quer, não como fazer.
- Foco: o que deve ser obtido.
- O “como” (o passo a passo) fica escondido dentro do sistema/biblioteca.
- É como fazer um pedido no restaurante: “quero uma pizza de calabresa”; você não dita a receita.
-
Exemplos de linguagens/tecnologias em estilo declarativo:
-
SQL (
SELECT ... WHERE ...) -
HTML (
<div>,<button>, etc. descrevendo a tela) -
Programação funcional (map/filter/reduce, React com JSX em boa parte)
Exemplo em SQL (declarativo):
SELECT nome
FROM clientes
WHERE ativo = 'S';
Você não diz “faça um loop na tabela clientes…”, só declara:
“quero os nomes dos clientes onde ativo = 'S'”.
No modo DECLARATIVO montamos as funções, conforme abaixo:
// MovimentoEstoque representa um ajuste no estoque.
// Delta negativo = saída, positivo = entrada.
type MovimentoEstoque struct {
ProdutoID int64
Delta int
// GerarMovimentosSaida: "para cada item do pedido, gere um movimento de saída".
func GerarMovimentosSaida(itens []ItemPedido) []MovimentoEstoque {
movimentos := make([]MovimentoEstoque, 0, len(itens))
for _, item := range itens {
if item.Qtd <= 0 {
continue // regra simples: ignora item com qtd inválida
}
movimentos = append(movimentos, MovimentoEstoque{
ProdutoID: item.ProdutoID,
Delta: -item.Qtd, // saída => negativo
})
}
return movimentos
}
// ConsolidarMovimentos agrupa por produto somando os deltas.
func ConsolidarMovimentos(movs []MovimentoEstoque) []MovimentoEstoque {
acc := make(map[int64]int)
for _, m := range movs {
acc[m.ProdutoID] += m.Delta
}
result := make([]MovimentoEstoque, 0, len(acc))
for produtoID, delta := range acc {
result = append(result, MovimentoEstoque{
ProdutoID: produtoID,
Delta: delta,
})
}
return result
}
// ValidarEstoqueSuficiente verifica se todos os movimentos podem ser aplicados.
func ValidarEstoqueSuficiente(estoque MapaEstoque, movimentos []MovimentoEstoque) error {
for _, m := range movimentos {
qtdAtual, ok := estoque[m.ProdutoID]
if !ok {
return fmt.Errorf("produto %d não encontrado no estoque", m.ProdutoID)
}
novo := qtdAtual + m.Delta // lembre: Delta pode ser negativo
if novo < 0 {
return fmt.Errorf("estoque insuficiente para produto %d: atual=%d, movimento=%d, resultaria em %d",
m.ProdutoID, qtdAtual, m.Delta, novo)
}
}
return nil
}
// AplicarMovimentos aplica os deltas no mapa de estoque.
func AplicarMovimentos(estoque MapaEstoque, movimentos []MovimentoEstoque) {
for _, m := range movimentos {
estoque[m.ProdutoID] += m.Delta
}
}
}```
Código Principal no estilo mais declarativo:
```go
// AtualizarEstoqueDeclarativo orquestra o fluxo de domínio.
func AtualizarEstoqueDeclarativo(estoque MapaEstoque, itens []ItemPedido) error {
// 1. Declara: "quero movimentos de saída a partir dos itens"
movimentos := GerarMovimentosSaida(itens)
// 2. Declara: "quero consolidar esses movimentos por produto"
movimentos = ConsolidarMovimentos(movimentos)
// 3. Declara: "verifique se o estoque suporta esses movimentos"
if err := ValidarEstoqueSuficiente(estoque, movimentos); err != nil {
return err
}
// 4. Declara: "aplique os movimentos"
AplicarMovimentos(estoque, movimentos)
return nil
}
movimentos := GerarMovimentosSaida(itens)
movimentos = ConsolidarMovimentos(movimentos)
if err := ValidarEstoqueSuficiente(estoque, movimentos); err != nil { ... }
AplicarMovimentos(estoque, movimentos)
Isso é quase “português” de regra de negócio:
“Gera movimentos de saída, consolida, valida, aplica.”
Os for, map, append e demais detalhes estão escondidos em helpers bem nomeados.
Como ficaria numa camada de serviço + repositório (DB real)
Imagina que em vez de MapaEstoque, você tem um repositório com Go + sqlx:
type EstoqueRepository interface {
// Busca o estoque atual de uma lista de produtos
BuscarPorProdutos(ctx context.Context, produtoIDs []int64) (MapaEstoque, error)
// Aplica uma lista de movimentos (pode ser um UPDATE por produto, ou um batch)
AplicarMovimentos(ctx context.Context, movimentos []MovimentoEstoque) error
}
Na camada de serviço, você poderia fazer algo assim (bem declarativo):
func (s *EstoqueService) BaixarEstoquePedido(ctx context.Context, itens []ItemPedido) error {
produtoIDs := extrairProdutoIDs(itens)
estoqueAtual, err := s.repo.BuscarPorProdutos(ctx, produtoIDs)
if err != nil {
return err
}
movimentos := GerarMovimentosSaida(itens)
movimentos = ConsolidarMovimentos(movimentos)
if err := ValidarEstoqueSuficiente(estoqueAtual, movimentos); err != nil {
return err
}
// Aqui dá pra envolver tudo em transação, etc.
if err := s.repo.AplicarMovimentos(ctx, movimentos); err != nil {
return err
}
return nil
}
Aqui a camada de serviço está bem declarativa, focada na regra de negócio,
e o repositório por baixo usa Go imperativo normal com SQL.
O que você ganha com esse estilo “misto”
- Na regra de negócio (service): código mais próximo de “linguagem ubíqua” de domínio.
- Nos helpers/repositórios: Go idiomático, loops explícitos, fácil de debugar, performático.
- Fica muito fácil evoluir a regra:
amanhã você quer suportar também movimentos de entrada?
Cria GerarMovimentosEntrada reaproveitando ConsolidarMovimentos, ValidarEstoqueSuficiente (talvez renomeie) e AplicarMovimentos.
quer logar todos os movimentos? Você mexe numa função só.
Exemplo Prático Simplificado
Versão Imperativa
func BaixarEstoqueImperativo(estoque map[int]int, pedido Pedido) error {
// 1) Verifica se o produto existe no estoque
qtdAtual, existe := estoque[pedido.ProdutoID]
if !existe {
return fmt.Errorf("produto %d não encontrado", pedido.ProdutoID)
}
// 2) Verifica se a quantidade é válida
if pedido.Quantidade <= 0 {
return fmt.Errorf("quantidade inválida: %d", pedido.Quantidade)
}
// 3) Verifica se tem estoque suficiente
if qtdAtual < pedido.Quantidade {
return fmt.Errorf("estoque insuficiente: atual=%d, solicitado=%d",
qtdAtual, pedido.Quantidade)
}
// 4) Atualiza o estoque (baixa)
estoque[pedido.ProdutoID] = qtdAtual - pedido.Quantidade
return nil
}
Uso no main:
func main() {
estoque := map[int]int{
10: 100,
}
pedido := Pedido{
ProdutoID: 10,
Quantidade: 3,
}
err := BaixarEstoqueImperativo(estoque, pedido)
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Println("Novo estoque do produto 10:", estoque[10]) // 97
}
Linha por linha:
“pega quantidade… testa… testa… atualiza…”.
Isso é bem imperativo: foco total no passo a passo.
Versão Declarativa
Quebramos a mesma rotina em funções com nomes claros, ou seja, funções pequenas bem nomeadas.
ValidarBaixaEstoque
func ValidarBaixaEstoque(estoque map[int]int, pedido Pedido) error {
qtdAtual, existe := estoque[pedido.ProdutoID]
if !existe {
return fmt.Errorf("produto %d não encontrado", pedido.ProdutoID)
}
if pedido.Quantidade <= 0 {
return fmt.Errorf("quantidade inválida: %d", pedido.Quantidade)
}
if qtdAtual < pedido.Quantidade {
return fmt.Errorf("estoque insuficiente: atual=%d, solicitada=%d",
qtdAtual, pedido.Quantidade)
}
return nil
}
AplicarBaixaEstoque
func AplicarBaixaEstoque(estoque map[int]int, pedido Pedido) {
estoque[pedido.ProdutoID] = estoque[pedido.ProdutoID] - pedido.Quantidade
}
BaixarEstoqueDeclarativo
func BaixarEstoqueDeclarativo(estoque map[int]int, pedido Pedido) error {
// 1) Declara a intenção: "validar se posso baixar"
if err := ValidarBaixaEstoque(estoque, pedido); err != nil {
return err
}
// 2) Declara a intenção: "aplicar a baixa"
AplicarBaixaEstoque(estoque, pedido)
return nil
}
Uso no main (igual ao outro, só trocando o nome):
func main() {
estoque := map[int]int{
10: 100,
}
pedido := Pedido{
ProdutoID: 10,
Quantidade: 3,
}
err := BaixarEstoqueDeclarativo(estoque, pedido)
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Println("Novo estoque do produto 10:", estoque[10]) // 97
}
Imperativo (primeira versão)
-
A função faz tudo:
- valida produto
- valida quantidade
- valida estoque
- atualiza o map
-
É fácil de entender, mas a regra de negócio fica num “blocão”.
Declarativo (primeira versão)
A função principal (BaixarEstoqueDeclarativo) parece uma frase:
if err := ValidarBaixaEstoque(...); err != nil { ... }
AplicarBaixaEstoque(...)
- Se lê assim:
- Primeiro valida se pode baixar;
- Depois aplica a baixa.
O "como" cada coisa é feita está escondido em funções pequenas e com nome claro.
Conclusão Final
Resumo
-
No código imperativo, você está dirigindo o carro: troca marcha, pisa no freio, vira o volante.
-
No código mais declarativo, você diz:
“verifica se pode baixar” e “baixa o estoque”.
Os detalhes de como isso é feito moram em funções separadas.
Arlei F. Farnetani Junior
farnetani@gmail.com