Skip to content
Deep Dive 9 min read

Por qué Zolo avisa con 4 niveles de anidamiento (y la idea de 60 años detrás de eso)

Una advertencia de lint sobre profundidad de anidamiento, rastreada a través de Linus Torvalds, la complejidad ciclomática de McCabe (1976), "Go To Considered Harmful" de Dijkstra (1968) y los límites de memoria de trabajo de Miller (1956).


Por qué Zolo avisa con 4 niveles de anidamiento (y la idea de 60 años detrás de eso)

🇺🇸 English version: Why Zolo Warns at 4 Levels of Nesting · 🇧🇷 Versão em português: Por que o Zolo avisa em 4 níveis de aninhamento

Escribes un poco de código Zolo. El compilador devuelve:

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)

Y piensas: "¿Quién decidió que es 4? ¿Por qué no 5? ¿Por qué no 8?"

La respuesta honesta es que el número exacto es arbitrario, pero la idea detrás de él es uno de los principios más antiguos y mejor validados de la ingeniería de software — se remonta a un artículo de psicología cognitiva de 1956, una carta de 1968 que prácticamente dio a luz a la programación estructurada, y una métrica de 1976 que aún hoy es usada por todo analizador estático serio.

Este post recorre el lint, su historia y lo que significa para la manera en que escribes Zolo.

Qué hace realmente el lint #

La regla max-nesting-depth vive en crates/zolo-compiler/src/lint.rs. La implementación cabe en una pantalla:

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,
        );
    }
}

Un visitor recorre la AST. Cada una de estas construcciones incrementa el contador:

  • if / else
  • match (y cada brazo)
  • for
  • while
  • loop
  • expresiones de control usadas como valor (let x = if ..., let x = match ...)

El cuerpo de la función en sí no cuenta. Entonces fn foo() { if a { if b { if c { if d { if e { ... } } } } } } produce depth 5 y dispara el warning.

El umbral por defecto es 4, configurable en el .lint.toml del proyecto. ¿Por qué 4? Rebobinemos 70 años.

1956 — Miller y los límites de la memoria de trabajo #

En The Magical Number Seven, Plus or Minus Two: Some Limits on Our Capacity for Processing Information (Miller, 1956), el psicólogo George A. Miller observó que los humanos pueden retener aproximadamente 7 ± 2 elementos discretos en la memoria de corto plazo simultáneamente.

El artículo original no es sobre código — Miller estudiaba percepción de tonos, sabores y recall de cadenas de dígitos. Pero el hallazgo fue adoptado por todos los campos posteriores que se ocupan de cómo los humanos procesan información. Investigaciones posteriores sobre carga cognitiva incluso bajaron el número — el trabajo de Nelson Cowan a principios de los 2000 sugiere que la capacidad real de la memoria de trabajo está más cerca de 4 ± 1 chunks (Cowan, 2001).

¿Por qué importa esto para el código anidado? Porque leer un bloque profundamente anidado no es un solo pensamiento. Para entender la línea N dentro de if A { for B { match C { if D { ... } } } }, tienes que sostener A, B, C y D simultáneamente en tu cabeza como contexto activo — y recordar qué significa cada uno en la lógica circundante. Cada nivel es un chunk. En 4–5 niveles ya estás al borde de lo que un humano puede rastrear sin perder el hilo.

🔖 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 y la programación estructurada #

En marzo de 1968, Edsger Dijkstra publicó una carta de una página y media en Communications of the ACM titulada Go To Statement Considered Harmful (Dijkstra, 1968). Es quizás el artículo más citado de la computación.

"El uso desenfrenado del go to tiene como consecuencia inmediata que se vuelve terriblemente difícil encontrar un conjunto significativo de coordenadas para describir el progreso del proceso."

El argumento central: a medida que el código crece en complejidad dinámica, los humanos pierden la capacidad de mapear mentalmente el código fuente al estado del programa. Defiende restringir el flujo de control a secuencia, selección (if) e iteración (while) — lo que hoy llamamos programación estructurada.

La carta de Dijkstra mató al goto en la práctica mainstream, pero la idea más profunda se aplica igual al anidamiento profundo: cada nivel adicional de estructura de control es una coordenada más que tienes que rastrear para saber dónde estás en el programa. Cinco niveles de if, en ese sentido, son apenas marginalmente mejores que goto — ambos dificultan responder "¿qué es verdadero aquí?"

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

1976 — McCabe le pone número a la complejidad #

En A Complexity Measure (McCabe, 1976), Thomas J. McCabe propuso una métrica de teoría de grafos: cuenta el número de caminos linealmente independientes a través del grafo de flujo de control de una función. La llamó complejidad ciclomática.

La fórmula es simple: V(G) = EN + 2P donde E son aristas, N son nodos, P son componentes conectados. Para código típico, cada if, while, for, case suma 1.

McCabe no midió el anidamiento directamente — pero su métrica y el anidamiento están profundamente correlacionados, porque cada rama anidada multiplica el conteo de caminos. Propuso 10 como límite superior blando para la complejidad ciclomática por función. En 5 niveles anidados de if ya quemaste ese presupuesto solo en estructura, sin espacio para la lógica real.

Esta es la métrica que vive, cuatro décadas después, dentro de SonarQube, Code Climate, la regla complexity de ESLint y cognitive_complexity de 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, directo como siempre #

Saltando al Linux Kernel Coding Style (Torvalds, actual). Sección 1, sobre indentación:

"La respuesta a eso es que si necesitas más de 3 niveles de indentación, ya estás jodido de todos modos, y deberías arreglar tu programa."

Linus no cita ni a McCabe ni a Miller. Ofrece la versión field-tested y práctica del mismo hallazgo: el código profundamente anidado es un síntoma — casi siempre síntoma de que una función está haciendo demasiadas cosas y debería ser dividida.

Vale la pena notar que él dice 3, no 4. El kernel usa tabs de 8 caracteres en parte para hacer esto físicamente doloroso: con 3 niveles ya estás 24 columnas hacia dentro.

Lo que eligió el resto del mundo #

Linter / Herramienta Regla Por defecto Notas
ESLint max-depth 4 "Profundidad máxima a la que se pueden anidar bloques"
Clippy (Rust) excessive_nesting opt-in (threshold por defecto 0) Lint de restricción; los usuarios típicamente ponen 5
SonarQube S134 3–4 (varía según lenguaje) Java/C++ por defecto 4, varios otros 3
Linux Kernel coding-style §1 3 "Si necesitas más, estás jodido"
Zolo max-nesting-depth 4 Configurable por proyecto

No hay consenso sobre el número exacto, pero la franja es estrecha: 3 a 5. Zolo eligió 4 porque coincide con ESLint (el linter más desplegado del planeta) y porque en la práctica traza la línea justo donde el código empieza a volverse más difícil de leer en voz alta.

Cómo se ve esto en Zolo #

Una función de 5 niveles (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),
        }
      }
    }
  }
}

La misma lógica, refactorizada con guard clauses y una función 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),
  }
}

Pasaron dos cosas:

  1. Guard clauses invirtieron los casos negativos en early returns, eliminando un nivel de if. Este patrón a veces se llama "return early, return often" y se remonta a Object-Oriented Software Construction de Bertrand Meyer (1988).
  2. Extracción movió el trabajo interno a handle, que nunca pasa de depth 2. El nombre de la función ahora documenta lo que antes era estructura enterrada.

El resultado se lee de arriba hacia abajo. Puedes parar en cualquier línea y saber qué es verdadero por encima.

Cuando la regla está equivocada #

Los lints codifican defaults, no absolutos. Casos en los que el anidamiento profundo es genuinamente la expresión más clara:

  • Parsers y tokenizadores — las gramáticas sensibles al contexto producen naturalmente árboles match profundos.
  • Máquinas de estado — el estado anidado explícito puede ser más claro que una tabla de despacho plana cuando las transiciones son escasas.
  • Kernels numéricos — los bucles apretados sobre arrays multidimensionales (procesamiento de imágenes, kernels de ML) suelen necesitar 4+ niveles solo de for.

Para esos, ajusta por proyecto en .lint.toml:

[settings]
max-nesting-depth = 6

[rules]
\ o deshabilítalo por completo
max-nesting-depth = "off"

El propósito del warning no es decir que 5 sea ilegal. Es que el default debe ser conservador, y cruzarlo debería ser una elección deliberada que puedas explicar.

Por qué Zolo se molesta #

Un lint no es una preferencia de estilo. Es un pequeño pedazo de revisión por pares automatizada que se ejecuta cada vez que compilas. El efecto acumulado, a lo largo de miles de ediciones, es que el código se mantiene dentro de la franja donde los humanos aún pueden razonar sobre él sin dedicarle un café a cada función.

Miller nos mostró el techo cognitivo. Dijkstra nos mostró por qué importa la estructura. McCabe nos dio una forma de medirla. Linus la tradujo a algo que un mantenedor diría de verdad.

El default de 4 de Zolo es un número en esa larga línea genealógica. Si alguna vez sientes el impulso de silenciar el warning, hazte primero la pregunta de Linus: ¿estoy jodido y debería arreglar mi programa?

La mayoría de las veces, la respuesta es sí.


Lectura 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. Regla max-depth
  • Rust Clippy. Lint excessive_nesting
  • SonarSource. Regla S134
enespt-br