Una introducción fácil al alcance léxico en JavaScript

El alcance léxico es un tema que asusta a muchos programadores. Una de las mejores explicaciones del alcance léxico se puede encontrar en el libro de Kyle Simpson You Don't Know JS: Scope and Closures. Sin embargo, incluso su explicación falta porque no usa un ejemplo real.

Uno de los mejores ejemplos reales de cómo funciona el alcance léxico, y por qué es importante, se puede encontrar en el famoso libro de texto “La estructura e interpretación de los programas de computadora” (SICP) de Harold Abelson y Gerald Jay Sussman. Aquí hay un enlace a una versión PDF del libro: SICP.

SICP usa Scheme, un dialecto de Lisp, y se considera uno de los mejores textos introductorios de informática jamás escritos. En este artículo, me gustaría volver a visitar su ejemplo de alcance léxico utilizando JavaScript como lenguaje de programación.

Nuestro ejemplo

El ejemplo que usaron Abelson y Sussman es calcular raíces cuadradas usando el método de Newton. El método de Newton funciona determinando aproximaciones sucesivas para la raíz cuadrada de un número hasta que la aproximación se encuentra dentro de un límite de tolerancia para ser aceptable. Trabajemos con un ejemplo, como hacen Abelson y Sussman en SICP.

El ejemplo que usan es hallar la raíz cuadrada de 2. Empiezas por adivinar la raíz cuadrada de 2, digamos 1. Mejoras esta suposición dividiendo el número original por la suposición y luego promediando ese cociente y la suposición actual de inventa la siguiente suposición. Te detienes cuando alcanzas un nivel aceptable de aproximación. Abelson y Sussman utilizan el valor 0,001. A continuación, se muestra un resumen de los primeros pasos del cálculo:

Square root to find: 2First guess: 1Quotient: 2 / 1 = 2Average: (2+1) / 2 = 1.5Next guess: 1.5Quotient: 1.5 / 2 = 1.3333Average: (1.3333 + 1.5) / 2 = 1.4167Next guess: 1.4167Quotient: 1.4167 / 2 = 1.4118Average: (1.4167 + 1.4118) / 2 = 1.4142

Y así sucesivamente hasta que la suposición se encuentre dentro de nuestro límite de aproximación, que para este algoritmo es 0.001.

Una función de JavaScript para el método de Newton

Después de esta demostración del método, los autores describen un procedimiento general para resolver este problema en Scheme. En lugar de mostrarle el código del esquema, lo escribiré en JavaScript:

function sqrt_iter(guess, x) { if (isGoodEnough(guess, x)) { return guess; } else { return sqrt_iter(improve(guess, x), x); }}

A continuación, necesitamos desarrollar varias otras funciones, incluidas isGoodEnough () yhance (), junto con algunas otras funciones auxiliares. Empezaremos con better (). Aquí está la definición:

function improve(guess, x) { return average(guess, (x / guess));}

Esta función usa una función auxiliar promedio (). Aquí está esa definición:

function average(x, y) { return (x+y) / 2;}

Ahora estamos listos para definir la función isGoodEnough (). Esta función sirve para determinar cuándo nuestra suposición está lo suficientemente cerca de nuestra tolerancia de aproximación (0.001). Aquí está la definición de isGoodEnough ():

function isGoodEnough(guess, x) { return (Math.abs(square(guess) - x)) < 0.001;}

Esta función usa una función square (), que es fácil de definir:

function square(x) { return x * x;}

Ahora todo lo que necesitamos es una función para comenzar:

function sqrt(x) { return sqrt_iter(1.0, x);}

Esta función usa 1.0 como una suposición inicial, lo que generalmente está bien.

Ahora estamos listos para probar nuestras funciones para ver si funcionan. Los cargamos en un shell JS y luego calculamos algunas raíces cuadradas:

> .load sqrt_iter.js> sqrt(3)1.7321428571428572> sqrt(9)3.00009155413138> sqrt(94 + 87)13.453624188555612> sqrt(144)12.000000012408687

Las funciones parecen estar funcionando bien. Sin embargo, hay una mejor idea al acecho aquí. Todas estas funciones están escritas de forma independiente, a pesar de que están diseñadas para trabajar en conjunto entre sí. Probablemente no vamos a utilizar la función isGoodEnough () con ningún otro conjunto de funciones, o por sí sola. Además, la única función que le importa al usuario es la función sqrt (), ya que es la que se llama para encontrar una raíz cuadrada.

Bloquear alcance oculta funciones auxiliares

La solución aquí es usar el alcance del bloque para definir todas las funciones auxiliares necesarias dentro del bloque de la función sqrt (). Vamos a eliminar el cuadrado () y el promedio () de la definición, ya que esas funciones podrían ser útiles en otras definiciones de funciones y no están tan limitadas para usar en un algoritmo que implementa el Método de Newton. Aquí está la definición de la función sqrt () ahora con las otras funciones auxiliares definidas dentro del alcance de sqrt ():

function sqrt(x) { function improve(guess, x) { return average(guess, (x / guess)); } function isGoodEnough(guess, x) { return (Math.abs(square(guess) - x)) > 0.001; } function sqrt_iter(guess, x) { if (isGoodEnough(guess, x)) { return guess; } else { return sqrt_iter(improve(guess, x), x); } } return sqrt_iter(1.0, x);}

Ahora podemos cargar este programa en nuestro shell y calcular algunas raíces cuadradas:

> .load sqrt_iter.js> sqrt(9)3.00009155413138> sqrt(2)1.4142156862745097> sqrt(3.14159)1.772581833543688> sqrt(144)12.000000012408687

Tenga en cuenta que no puede llamar a ninguna de las funciones auxiliares desde fuera de la función sqrt ():

> sqrt(9)3.00009155413138> sqrt(2)1.4142156862745097> improve(1,2)ReferenceError: improve is not defined> isGoodEnough(1.414, 2)ReferenceError: isGoodEnough is not defined

Dado que las definiciones de estas funciones (mejorar () e isGoodEnough ()) se han movido dentro del alcance de sqrt (), no se puede acceder a ellas en un nivel superior. Por supuesto, puede mover cualquiera de las definiciones de función auxiliar fuera de la función sqrt () para tener acceso a ellas globalmente como hicimos con average () y square ().

Hemos mejorado enormemente nuestra implementación del método de Newton, pero todavía hay una cosa más que podemos hacer para mejorar nuestra función sqrt () simplificándola aún más aprovechando el alcance léxico.

Mejorando la claridad con Lexical Scope

El concepto detrás del alcance léxico es que cuando una variable está vinculada a un entorno, otros procedimientos (funciones) que están definidos en ese entorno tienen acceso al valor de esa variable. Esto significa que en la función sqrt (), el parámetro x está vinculado a esa función, lo que significa que cualquier otra función definida dentro del cuerpo de sqrt () puede acceder a x.

Sabiendo esto, podemos simplificar aún más la definición de sqrt () eliminando todas las referencias a x en las definiciones de funciones, ya que x es ahora una variable libre y accesible para todos. Aquí está nuestra nueva definición de sqrt ():

function sqrt(x) { function isGoodEnough(guess) { return (Math.abs(square(guess) - x)) > 0.001; } function improve(guess) { return average(guess, (x / guess)); } function sqrt_iter(guess) { if (isGoodEnough(guess)) { return guess; } else { return sqrt_iter(improve(guess)); } } return sqrt_iter(1.0);}

Las únicas referencias al parámetro x están en cálculos donde se necesita el valor de x. Carguemos esta nueva definición en el shell y probémosla:

> .load sqrt_iter.js> sqrt(9)3.00009155413138> sqrt(2)1.4142156862745097> sqrt(123+37)12.649110680047308> sqrt(144)12.000000012408687

El alcance léxico y la estructura de bloques son características importantes de JavaScript y nos permiten construir programas que son más fáciles de entender y administrar. Esto es especialmente importante cuando comenzamos a construir programas más grandes y complejos.