Skip to content
Deep Dive 9 min read

Por que o Zolo avisa em 4 níveis de aninhamento (e a ideia de 60 anos por trás disso)

Um warning de lint sobre profundidade de aninhamento, rastreado por Linus Torvalds, a complexidade ciclomática de McCabe (1976), "Go To Considered Harmful" de Dijkstra (1968) e os limites de memória de trabalho de Miller (1956).


Por que o Zolo avisa em 4 níveis de aninhamento (e a ideia de 60 anos por trás disso)

🇺🇸 English version: Why Zolo Warns at 4 Levels of Nesting · 🇪🇸 Versión en español: Por qué Zolo avisa con 4 niveles de anidamiento

Você escreve um código em Zolo. O compilador devolve:

warning[max-nesting-depth]: nesting depth 5 exceeds max recommended (4)
warning[max-nesting-depth]: nesting depth 6 exceeds max recommended (4)
warning[max-nesting-depth]: nesting depth 7 exceeds max recommended (4)

Você pensa: "Quem decidiu que é 4? Por que não 5? Por que não 8?"

A resposta honesta é que o número exato é arbitrário, mas a ideia por trás dele é um dos princípios mais antigos e mais validados da engenharia de software — remonta a um artigo de psicologia cognitiva de 1956, uma carta de 1968 que praticamente deu à luz a programação estruturada, e uma métrica de 1976 ainda usada hoje por todo analisador estático sério.

Este post passa pelo lint, pela história, e pelo que isso significa para a forma como você escreve Zolo.

O que o lint realmente faz #

A regra max-nesting-depth mora em crates/zolo-compiler/src/lint.rs. A implementação cabe em uma tela:

fn enter_nesting(&mut self, span: Span) {
    self.nesting_depth += 1;
    let max = self.config.settings.max_nesting_depth;
    if self.nesting_depth > max {
        self.emit(
            "max-nesting-depth",
            format!(
                "nesting depth {} exceeds max recommended ({})",
                self.nesting_depth, max
            ),
            span,
            LintLevel::Warn,
        );
    }
}

Um visitor percorre a AST. Cada uma dessas construções incrementa o contador:

  • if / else
  • match (e cada arm)
  • for
  • while
  • loop
  • expressões de controle usadas como valor (let x = if ..., let x = match ...)

O corpo da função em si não conta. Então fn foo() { if a { if b { if c { if d { if e { ... } } } } } } produz depth 5 e dispara o warning.

O limite padrão é 4, configurável no .lint.toml do projeto. Por que 4? Vamos voltar 70 anos.

1956 — Miller e os limites da memória de trabalho #

Em The Magical Number Seven, Plus or Minus Two: Some Limits on Our Capacity for Processing Information (Miller, 1956), o psicólogo George A. Miller observou que humanos conseguem segurar, em média, 7 ± 2 itens discretos na memória de curto prazo ao mesmo tempo.

O artigo original não é sobre código — Miller estudava percepção de tons, sabores, e recall de sequências de dígitos. Mas a descoberta foi adotada por todo campo posterior que se importa com processamento humano de informação. Pesquisas seguintes em carga cognitiva inclusive abaixaram o número — o trabalho de Nelson Cowan no início dos anos 2000 sugere que a capacidade real da memória de trabalho está mais perto de 4 ± 1 chunks (Cowan, 2001).

Por que isso importa para código aninhado? Porque ler um bloco profundamente aninhado não é um único pensamento. Para entender a linha N dentro de if A { for B { match C { if D { ... } } } }, você precisa segurar A, B, C e D simultaneamente na cabeça como contexto ativo — e lembrar o que cada um significa na lógica em volta. Cada nível é um chunk. Em 4–5 níveis, você já está na borda do que um humano consegue rastrear sem perder o fio.

🔖 Miller, G. A. (1956). The magical number seven, plus or minus two: some limits on our capacity for processing information. Psychological Review, 63(2), 81–97. DOI: 10.1037/h0043158

1968 — Dijkstra e a programação estruturada #

Em março de 1968, Edsger Dijkstra publicou uma carta de uma página e meia na Communications of the ACM intitulada Go To Statement Considered Harmful (Dijkstra, 1968). É talvez o artigo mais citado da computação.

"O uso desenfreado do go to tem como consequência imediata que se torna terrivelmente difícil encontrar um conjunto significativo de coordenadas para descrever o progresso do processo."

O argumento central: à medida que o código cresce em complexidade dinâmica, humanos perdem a capacidade de mapear mentalmente código-fonte para estado do programa. Ele defende restringir o fluxo de controle a sequência, seleção (if) e iteração (while) — o que hoje chamamos de programação estruturada.

A carta do Dijkstra matou o goto na prática mainstream, mas o insight mais profundo se aplica igualmente ao aninhamento profundo: cada nível adicional de estrutura de controle é mais uma coordenada que você precisa rastrear para saber onde está no programa. Cinco níveis de if, nesse sentido, é só marginalmente melhor que goto — ambos dificultam responder "o que é verdade aqui?"

🔖 Dijkstra, E. W. (1968). Go to statement considered harmful. Communications of the ACM, 11(3), 147–148. DOI: 10.1145/362929.362947 — também arquivado como EWD215.

1976 — McCabe dá um número à complexidade #

Em A Complexity Measure (McCabe, 1976), Thomas J. McCabe propôs uma métrica teórica-de-grafos: conte o número de caminhos linearmente independentes pelo grafo de fluxo de controle de uma função. Ele chamou de complexidade ciclomática.

A fórmula é simples: V(G) = EN + 2P onde E é arestas, N é nós, P é componentes conectados. Para código típico, cada if, while, for, case soma 1.

McCabe não mediu aninhamento diretamente — mas a métrica dele e aninhamento são profundamente correlacionados, porque cada branch aninhada multiplica a contagem de caminhos. Ele propôs 10 como limite superior suave para complexidade ciclomática por função. Em 5 níveis aninhados de if, você já queimou esse orçamento só em estrutura, sem espaço para a lógica em si.

Essa é a métrica que vive, quatro décadas depois, dentro do SonarQube, Code Climate, regra complexity do ESLint, e cognitive_complexity do Clippy.

🔖 McCabe, T. J. (1976). A complexity measure. IEEE Transactions on Software Engineering, SE-2(4), 308–320. DOI: 10.1109/TSE.1976.233837

2001 — Torvalds, direto como sempre #

Pulando para o Linux Kernel Coding Style (Torvalds, atual). Seção 1, sobre indentação:

"A resposta para isso é que se você precisa de mais de 3 níveis de indentação, você está ferrado de qualquer jeito, e deveria consertar seu programa."

Linus não cita McCabe nem Miller. Ele oferece a versão field-tested e prática da mesma descoberta: código profundamente aninhado é um sintoma — quase sempre sintoma de que uma função está fazendo coisas demais e deveria ser dividida.

Vale notar que ele diz 3, não 4. O kernel usa tabs de 8 caracteres em parte para tornar isso fisicamente doloroso: em 3 níveis você já está 24 colunas para dentro.

O que o resto do mundo escolheu #

Linter / Ferramenta Regra Padrão Notas
ESLint max-depth 4 "Profundidade máxima que blocos podem ser aninhados"
Clippy (Rust) excessive_nesting opt-in (threshold padrão 0) Lint de restrição; usuários tipicamente colocam 5
SonarQube S134 3–4 (varia por linguagem) Java/C++ padrão 4, vários outros 3
Linux Kernel coding-style §1 3 "Se você precisa de mais, você está ferrado"
Zolo max-nesting-depth 4 Configurável por projeto

Não há consenso sobre o número exato, mas a faixa é estreita: 3 a 5. Zolo escolheu 4 porque casa com o ESLint (o linter mais difundido do planeta) e porque, na prática, traça a linha exatamente onde o código começa a ficar mais difícil de ler em voz alta.

Como isso fica em Zolo #

Uma função de 5 níveis (warning):

fn process(items) {
  if items != nil {                    \ depth 1
    for item in items {                \ depth 2
      if item.valid {                  \ depth 3
        match item.kind {              \ depth 4
          "x" => if item.urgent {      \ depth 5 → warning
            do_urgent_x(item)
          },
          _ => skip(item),
        }
      }
    }
  }
}

A mesma lógica, refatorada com guard clauses e uma função extraída:

fn process(items) {
  if items == nil { return }
  for item in items {
    handle(item)
  }
}

fn handle(item) {
  if !item.valid { return }
  match item.kind {
    "x" if item.urgent => do_urgent_x(item),
    "x" => {},
    _ => skip(item),
  }
}

Duas coisas aconteceram:

  1. Guard clauses inverteram casos negativos em early returns, removendo um nível de if. Esse padrão é às vezes chamado de "return early, return often" e remonta ao Object-Oriented Software Construction de Bertrand Meyer (1988).
  2. Extração moveu o trabalho interno para handle, que nunca passa de depth 2. O nome da função agora documenta o que antes era estrutura enterrada.

O resultado lê de cima para baixo. Você pode parar em qualquer linha e saber o que é verdade acima.

Quando a regra está errada #

Lints codificam padrões, não absolutos. Casos em que aninhamento profundo é genuinamente a expressão mais clara:

  • Parsers e tokenizers — gramáticas sensíveis a contexto naturalmente produzem árvores profundas de match.
  • Máquinas de estado — estado aninhado explícito pode ser mais claro do que uma tabela de dispatch plana quando transições são esparsas.
  • Kernels numéricos — loops apertados sobre arrays multi-dimensionais (processamento de imagem, kernels de ML) frequentemente precisam de 4+ níveis só de for.

Para esses, ajuste por projeto no .lint.toml:

[settings]
max-nesting-depth = 6

[rules]
\ ou desabilite completamente
max-nesting-depth = "off"

A intenção do warning não é dizer que 5 é ilegal. É que o padrão deve ser conservador, e ultrapassá-lo deveria ser uma escolha deliberada que você consegue explicar.

Por que o Zolo se preocupa com isso #

Um lint não é uma preferência de estilo. É um pequeno pedaço de revisão de pares automatizada que roda toda vez que você compila. O efeito cumulativo, ao longo de milhares de edits, é que o código permanece dentro da faixa onde humanos ainda conseguem raciocinar sobre ele sem dedicar um café para cada função.

Miller nos mostrou o teto cognitivo. Dijkstra nos mostrou por que estrutura importa. McCabe nos deu um jeito de medir. Linus traduziu para algo que um mantenedor diria de verdade.

O padrão 4 do Zolo é um número nessa longa linhagem. Se você sentir vontade de silenciar o warning, faça primeiro a pergunta do Linus: eu estou ferrado, e deveria consertar meu programa?

Na maioria das vezes, a resposta é sim.


Leitura adicional #

  • Miller, G. A. (1956). The magical number seven, plus or minus two. PDF · DOI
  • Dijkstra, E. W. (1968). Go to statement considered harmful. PDF · EWD215
  • McCabe, T. J. (1976). A complexity measure. PDF
  • Cowan, N. (2001). The magical number 4 in short-term memory. DOI
  • Torvalds, L. Linux kernel coding style. kernel.org
  • ESLint. Regra max-depth
  • Rust Clippy. Lint excessive_nesting
  • SonarSource. Regra S134
enespt-br