Clase 082 — Alcance (scope) y sombreado (shadowing)

Parte 5 — Funciones y modularidad · ⏱️ Duración estimada: 90 min · Nivel: IntermedioClase construida — 10 implementaciones del núcleo verificadas contra casos.json.


🎯 Objetivo

Comprender dónde vive un nombre. Cada vez que escribes x, el lenguaje tiene que decidir a qué variable te refieres, y esa decisión no es libre: sigue una regla precisa llamada alcance (scope), la región del texto del programa donde ese nombre es visible. El alcance es la pieza que convierte un montón de variables sueltas en un sistema con compartimentos, donde lo que ocurre dentro de una función o un bloque no contamina lo de fuera. Sin él, cada i de cada bucle y cada temp de cada cálculo competirían por el mismo espacio y el programa sería un campo minado.

La idea profunda la formalizan Abelson y Sussman en Structure and Interpretation of Computer Programs (§3.2) con el modelo de entornos (environment model): un programa en ejecución no tiene «una tabla de variables», sino una cadena de entornos encadenados, donde cada bloque o cada llamada a función crea un marco nuevo que apunta al de fuera. Buscar el valor de x significa recorrer esa cadena desde el marco más interno hacia afuera hasta encontrar la primera ligadura con ese nombre. El sombreado (shadowing) es el corolario directo: si un marco interno declara su propio x, la búsqueda se detiene ahí y nunca llega al x externo, que sigue existiendo intacto, solo que oculto mientras dure el marco interior. Al salir del bloque, el marco desaparece y el x externo vuelve a ser visible.

Robert Sebesta, en Concepts of Programming Languages, contrapone dos formas de resolver esa cadena: el alcance léxico (o estático) —la ligadura se decide por dónde está escrito el código, y es la que usan los diez lenguajes de esta clase— frente al alcance dinámico, hoy casi extinto, donde se decidía por quién llamó a quién en tiempo de ejecución. Que hoy demos por sentado el alcance léxico es una conquista: hace que puedas leer un fragmento y saber a qué se refiere cada nombre sin ejecutar nada.

🧩 Situación

Estás depurando una función de cientos de líneas. En la parte de arriba hay un total que acumula el importe de una factura. A mitad del cuerpo, dentro de un for que recorre los descuentos, alguien declaró otro total para sumar solo los descuentos de esa iteración. El programa imprime al final un total que no cuadra, y pasas una hora buscando el error en la aritmética cuando el problema es de visibilidad: dentro del bucle, el nombre total estaba sombreado, y el total externo nunca recibió lo que creías. Este es el dolor clásico que el alcance provoca cuando no se entiende: la sensación de que «mi variable no cambió» cuando en realidad estabas escribiendo en otra variable que casualmente se llama igual. Entender el sombreado convierte ese misterio en una regla mecánica: el nombre más interno gana, y solo dentro de su bloque.

🧮 Modelo

Especificación y verificación en casos.json:

stdin esperado
5 interno=15 externo=5
0 interno=10 externo=0
-3 interno=7 externo=-3

📖 Definiciones y características

📐 Algoritmo (pseudocódigo neutral)

LEER n ; x <- n
BLOQUE: x_interno <- x + 10 ; imprimir interno
imprimir externo (x sigue siendo n)

🌐 Implementaciones idiomáticas — el código a la vista

Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json. Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.

Python · python/main.py · python main.py

import sys

n = int(sys.stdin.readline())
x = n
# Python no crea alcance de bloque: se usa otra variable para el 'interno'.
x_interno = x + 10
print(f"interno={x_interno} externo={x}")

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

JavaScript · javascript/main.mjs · node main.mjs

import { readFileSync } from "node:fs";

const n = parseInt(readFileSync(0, "utf8").trim(), 10);
const x = n;
{
  const x = n + 10; // sombrea a la externa dentro del bloque
  console.log(`interno=${x} externo=${n}`);
}

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

TypeScript · typescript/main.ts · pnpm exec tsx main.ts

import { readFileSync } from "node:fs";

const n: number = parseInt(readFileSync(0, "utf8").trim(), 10);
const x: number = n;
{
  const x: number = n + 10;
  console.log(`interno=${x} externo=${n}`);
}

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

Java · java/Main.java · java Main.java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine().trim());
        int x = n;
        {
            int xInterno = x + 10; // Java no permite re-declarar x en el bloque
            System.out.println("interno=" + xInterno + " externo=" + x);
        }
    }
}

🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure

C# · csharp/Program.cs · dotnet run

using System;

int n = int.Parse(Console.In.ReadToEnd().Trim());
int x = n;
{
    int xInterno = x + 10;
    Console.WriteLine($"interno={xInterno} externo={x}");
}

🧬 El mismo programa en la familia .NET: F# · VB.NET

Go · go/main.go · go run main.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

func main() {
    line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
    n, _ := strconv.Atoi(strings.TrimSpace(line))
    x := n // externo
    interno := 0
    {
        x := x + 10 // sombrea a la externa en este bloque
        interno = x
    }
    fmt.Printf("interno=%d externo=%d\n", interno, x)
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

Rust · rust/main.rs · rustc main.rs -o main && ./main

use std::io::Read;

fn main() {
    let mut s = String::new();
    std::io::stdin().read_to_string(&mut s).unwrap();
    let n: i64 = s.trim().parse().unwrap();
    let x = n;
    {
        let x = n + 10; // sombreado idiomático en Rust
        println!("interno={x} externo={n}");
    }
    let _ = x;
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

C · c/main.c · cc main.c -o main && ./main

#include <stdio.h>

int main(void) {
    long n;
    if (scanf("%ld", &n) != 1) return 1;
    long x = n;
    {
        long x = n + 10; /* sombrea a la externa dentro del bloque */
        printf("interno=%ld externo=%ld\n", x, n);
    }
    return 0;
}

🧬 El mismo programa en la familia C / llaves: C++ · Objective-C

SQL · sql/main.sql · sqlite3 :memory: < main.sql

-- SQL usa alias/subconsultas para acotar nombres.
WITH nums(n) AS (VALUES (5), (0), (-3))
SELECT printf('interno=%d externo=%d', n + 10, n) AS resultado FROM nums;

🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog

PHP · php/main.php · php main.php

<?php
$n = (int) trim(fgets(STDIN));
$x = $n;
// PHP no tiene alcance de bloque: se usa otra variable.
$xInterno = $x + 10;
echo "interno=$xInterno externo=$x\n";

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.

🔬 Ejemplo trabajado — del stdin a la salida

Sigamos el primer caso de casos.json (stdin = "5", esperado = "interno=15 externo=5") por tres lenguajes que tratan el bloque de tres maneras muy distintas.

Rust — el sombreado como herramienta idiomática. Tras parsear, let x = n; liga x a 5 en el marco de main. Al entrar en el bloque { ... }, la línea let x = n + 10; no reasigna: crea una nueva variable x en un marco interior, ligada a 15, que sombrea a la anterior. El println!("interno={x} externo={n}") de dentro del bloque ve el x interior (15) e imprime interno=15, mientras que n sigue valiendo 5. Al cerrar el bloque, ese x interior desaparece y la última línea let _ = x; vuelve a referirse al x exterior (5) —está ahí solo para que el compilador no avise de variable sin usar—. Rust convierte el sombreado en un idiom deliberado: puedes reintroducir un nombre e incluso cambiarle el tipo con un nuevo let, algo que Java o C prohíben.

Go — bloque explícito con :=. Aquí x := n liga el x externo a 5. Fíjate en el detalle: el resultado interno no se imprime dentro del bloque, sino que se guarda en interno, declarada fuera. Dentro del { }, x := x + 10 declara con := un x nuevo del bloque; su lado derecho lee todavía el x externo (5) y produce 15, que se asigna a interno. Go permite ese sombreado con := y de hecho es una fuente conocida de bugs (el go vet -vettool=shadow existe justamente para detectarlo). Al salir, fmt.Printf("interno=%d externo=%d\n", interno, x) usa interno=15 y el x externo, que nunca cambió: 5. Salida: interno=15 externo=5.

Python — sin alcance de bloque. Python es la excepción reveladora del grupo: un if o un for no crean un alcance nuevo; solo las funciones (y las comprensiones) lo hacen. Por eso la implementación no puede «abrir un bloque y redeclarar x»: si lo hiciera, machacaría el x de fuera, porque sería la misma variable. La solución idiomática es honesta con esa limitación y usa otro nombre, x_interno = x + 10, que con n = 5 vale 15. El f-string f"interno={x_interno} externo={x}" imprime interno=15 externo=5. Los tres llegan al mismo resultado, pero solo Rust y Go pudieron reutilizar el nombre x; Python tuvo que inventar uno nuevo porque su modelo de alcance no ofrece el compartimento que el sombreado necesita.

🔬 Comparación

Clase de diferencia Observación entre lenguajes
Sintáctica El bloque se marca con { } en C, Java, C#, JS/TS, Go y Rust; Python y PHP no tienen bloque léxico y delimitan con indentación o simplemente otra sentencia.
Semántica Rust y Go permiten redeclarar un nombre que sombrea al externo (let/:=); Java prohíbe redeclarar una variable local ya visible y obliga a otro nombre (xInterno); C y JS/TS con let/const sí sombrean por bloque.
Semántica JS distingue var (alcance de función) de let/const (alcance de bloque); usar let es lo que hace posible el sombreado del ejemplo. Python ofrece global/nonlocal para escribir en alcances externos a propósito.
Semántica Rust va más lejos: el sombreado puede cambiar el tipo de la variable, no solo el valor, algo imposible en los lenguajes de tipado estático con reasignación.
Paradigmática SQL no tiene variables de bloque: acota nombres con alias de columna y subconsultas/CTE; el WITH nums(n) cumple el papel del alcance.

La síntesis la da el modelo de entornos de SICP §3.2: no hay «magia» en que la externa reaparezca. Cada bloque es un marco enganchado al de fuera; la resolución de un nombre recorre la cadena de dentro hacia afuera y se queda con la primera coincidencia. Sombrear es añadir una ligadura más cerca; restaurar es descartar ese marco. Todos los lenguajes implementan esa misma cadena; solo difieren en qué construcciones abren un marco nuevo (Python: solo funciones; el resto: cada { }).

🧬 El concepto en la familia

En Kotlin, redeclarar con val/var dentro de un bloque interno sombrea al externo de forma idiomática, igual que en Rust. En Swift ocurre lo mismo con let. En Ruby, en cambio, los bloques comparten el alcance del método que los rodea (una variable ya existente se ve dentro del bloque), pero los parámetros del bloque sí crean nombres locales que pueden sombrear. En C++ el sombreado por bloque es habitual y los compiladores lo avisan con -Wshadow. Reconocer, en un lenguaje nuevo, qué construcción abre un alcance —¿el if? ¿el for? ¿solo la función?— es lo que te permite predecir si un nombre reutilizado sombreará o reasignará.

✅ Prueba común

Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:

python scripts/verificar_equivalencia.py 082

🧪 Reto de transferencia

Detalle en reto.md.

⚠️ Errores comunes

❓ Preguntas frecuentes

🔗 Referencias

Libros de la parte:

Libros de los lenguajes del núcleo:


⏮️ Clase 081 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 083 ⏭️