Engenharia iOS

Declarativo vs. imperativo: entendendo o SwiftUI

A primeira linha da documentação introdutória do SwiftUI inevitavelmente menciona um “framework declarativo”. React e Jetpack Compose também se descrevem como declarativos.

4 min de leitura
Imagem de capa de Declarativo vs. imperativo: entendendo o SwiftUI

A primeira linha da documentação introdutória do SwiftUI inevitavelmente menciona um “framework declarativo”. React e Jetpack Compose também se descrevem como declarativos.

Mas, quando alguém pergunta “o que significa declarativo?”, a resposta não é tão simples. “É código limpo” não está correto.

A diferença entre declarativo e imperativo se resume a uma frase. Você escreve “How” ou “What”?

Este artigo aborda esse critério com exemplos do dia a dia, confirma a diferença com código UIKit e SwiftUI e mostra que o declarativo não é gratuito.

Este é o resumo principal.

  1. Imperativo: instruir passo a passo o procedimento para chegar ao resultado desejado — How
  2. Declarativo: descrever o resultado desejado e deixar o procedimento para o sistema — What
  3. SQL, HTML, map/filter e SwiftUI são exemplos representativos de programação declarativa
  4. A essência da UI declarativa: declarar “quando o estado for este, a tela será assim”, deixando as atualizações decorrentes da mudança de estado para o framework

Uma analogia com um táxi

No modelo imperativo, você orienta o motorista diretamente: “vire à direita, siga por 300 metros, vire à esquerda no sinal…”. Você instrui cada etapa, e a soma delas produz o resultado de chegar ao destino.

No modelo declarativo, você diz: “Me leve à estação Gangnam”. Informa apenas o destino (What) e deixa a escolha da rota (How) para o motorista e o navegador.

Os dois chegam ao destino. A diferença é se eu sou dono do procedimento ou apenas da descrição do resultado.


No código, a mesma tarefa pode ser escrita de duas formas

Vamos escrever de duas formas o código que seleciona números pares e os eleva ao quadrado.

// Imperativo: eu indico como iterar e onde armazenar o resultado
var result: [Int] = []
for n in numbers {
    if n % 2 == 0 {
        result.append(n * n)
    }
}

// Declarativo: descrevo apenas o que quero
let result = numbers.filter { $0 % 2 == 0 }.map { $0 * $0 }

A versão imperativa expõe as “peças do procedimento”, como a variável do loop, o array intermediário e a ordem. A versão declarativa deixa apenas a intenção: “filtrar os pares e elevá-los ao quadrado”. O método de iteração fica oculto dentro de filter e map.

Você provavelmente já usa formas declarativas mais conhecidas. Em SQL, escreve apenas “quero as linhas com estas condições”, não como pesquisar o índice. Em HTML, declara a estrutura —“um título aqui, um parágrafo ali”—, não o procedimento de renderização.


Na UI, a diferença aumenta drasticamente

A UI imperativa (UIKit) usa procedimentos para alterar a tela.

// UIKit: Instruir diretamente as operações da tela sempre que o estado mudar
func updateBadge(count: Int) {
    if count > 0 {
        badgeLabel.isHidden = false
        badgeLabel.text = "\(count)"
    } else {
        badgeLabel.isHidden = true
    }
}

A dificuldade é que o desenvolvedor precisa acompanhar continuamente “em que estado a tela atual está”. Quando há vários caminhos de atualização, é fácil esquecer um deles, gerando o bug clássico: “os dados mudaram, mas a tela continua igual”.

A UI declarativa (SwiftUI) declara como a tela deve aparecer para determinado estado.

// SwiftUI: countDeclarar que, quando o estado for este, a tela será assim
struct BadgeView: View {
    let count: Int
    var body: some View {
        if count > 0 {
            Text("\(count)").badgeStyle()
        }
    }
}

Quando count muda, você não escreve como corrigir a tela. O SwiftUI compara a declaração anterior com a nova e atualiza apenas as partes necessárias. A UI vira uma “função do estado”. Como o framework assume a propriedade do procedimento de atualização, não há código de atualização para esquecer.

No imperativo, eu escrevo o procedimento de atualização; no declarativo, o framework calcula a diferença
No imperativo, eu escrevo o procedimento de atualização; no declarativo, o framework calcula a diferença

O declarativo não é gratuito

Para manter o equilíbrio, também precisamos olhar o outro lado.

Esconder o procedimento é frustrante quando você precisa dele. Um pedido imperativo como “mova a rolagem exatamente para este offset” pode exigir um contorno em um framework declarativo.

A depuração tem uma natureza diferente. No imperativo, você acompanha o procedimento; no declarativo, precisa rastrear as decisões do framework, perguntando “por que foi redesenhado?”. Não dá para ignorar completamente o How oculto.

O ajuste de desempenho acaba exigindo entender os internos. Quando o SwiftUI recalcula views em excesso, é preciso entender como diffing e rastreamento de dependências funcionam para resolver o problema.

Por isso, a perspectiva correta não é “o declarativo é superior”, mas uma escolha do nível de abstração. Transferir a propriedade do procedimento simplifica o código em torno da intenção, mas deixa o controle detalhado e o entendimento interno como desafios.

O trade-off de transferir a propriedade do procedimento é abrir mão do controle detalhado
O trade-off de transferir a propriedade do procedimento é abrir mão do controle detalhado

Resumo

  • O imperativo instrui o procedimento para chegar ao resultado (How), enquanto o declarativo descreve o resultado desejado (What)
  • SQL, HTML e map/filter já são formas declarativas conhecidas
  • A UI imperativa escreve os procedimentos que alteram a tela e tende a gerar bugs de inconsistência entre estado e tela
  • A UI declarativa declara “quando o estado for este, a tela será assim”, e o framework cuida das atualizações (UI = função do estado)
  • O custo do declarativo: o controle detalhado dos procedimentos é difícil e, no fim, é necessário entender o funcionamento interno do framework
  • A essência não é superioridade, mas escolher quem possui o procedimento