¡Feliz año nuevo, y bienvenidos de nuevo a Ruby Magic! En este episodio de invierno, nos sumergiremos en las fijaciones y los alcances. Así que ponte los esquís y síguenos en lo más profundo del bosque.
La última vez, vimos los cierres en Ruby comparando bloques, procs y lambdas. Aparte de las diferencias entre los tres tipos, tocamos lo que define un cierre.
Un cierre es una función de primera clase con un entorno. El entorno es un mapeo de las variables que existían cuando se creó el cierre. El cierre retiene su acceso a estas variables, incluso si están definidas en otro ámbito.
Hemos explorado el equivalente de Ruby a las funciones de primera clase, pero nos hemos saltado convenientemente los entornos. En este episodio, veremos cómo funciona ese entorno para los cierres, las clases y las instancias de clase, examinando cómo Ruby maneja el ámbito léxico a través de sus bindings.
Ámbito léxico
En programación, el ámbito se refiere a los bindings disponibles en una parte específica del código. Una vinculación, o vinculación de nombre, vincula un nombre a una referencia de memoria, como el nombre de una variable a su valor. El ámbito define lo que self
significa, los métodos que pueden ser llamados, y las variables que están disponibles.
Ruby, como la mayoría de los lenguajes de programación modernos, utiliza un ámbito estático, a menudo llamado ámbito léxico (en contraposición al ámbito dinámico). El ámbito actual se basa en la estructura del código y determina las variables disponibles en partes específicas del código. Esto significa que el ámbito cambia cuando el código salta entre métodos, bloques y clases, ya que todos ellos pueden tener diferentes variables locales, por ejemplo.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
En este método, creamos una variable local dentro de un método y la imprimimos en la consola. La variable está en el ámbito del método, ya que se crea allí.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
En este ejemplo, creamos la variable fuera del método. Cuando llamamos a la variable dentro de un método, obtenemos un error, ya que la variable está fuera de ámbito. Las variables locales tienen un ámbito estricto, lo que significa que un método no puede acceder a una variable fuera de sí mismo a menos que se pase como argumento.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Mientras que las variables locales están disponibles localmente, las variables de instancia están disponibles para todos los métodos de una instancia de clase.
Ámbitos heredados y Proc Bindings
Como hemos visto en los ejemplos anteriores, el ámbito se basa en la ubicación en el código. Una variable local definida fuera de un método no tiene alcance dentro del método, pero puede estar disponible convirtiéndola en una variable de instancia. Los métodos no pueden acceder a las variables locales definidas fuera de ellos porque los métodos tienen su propio ámbito, con sus propios enlaces.
Los proc (incluyendo los bloques y los lambda, por extensión) son diferentes. Siempre que se instancia un proc, se crea un binding que hereda las referencias a las variables locales del contexto en el que se creó el bloque.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
En este ejemplo, establecemos una variable llamada foo
a 1
. Internamente, el objeto Proc creado en la segunda línea crea un nuevo enlace. Cuando llamamos al proc, podemos pedir el valor de la variable.
Como el binding se crea cuando se inicializa el proc, no podemos crear el proc antes de definir la variable, aunque el bloque no se llame hasta después de definir la variable.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
La llamada al proc producirá un NameError
ya que la variable no está definida en los enlaces del proc. Por tanto, cualquier variable a la que se acceda en un proc debería definirse antes de crear el proc o pasarlo como argumento.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
Podemos, sin embargo, cambiar la variable después de que haya sido definida en el contexto principal ya que el enlace del proc mantiene una referencia a ella en lugar de copiarla.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
En este ejemplo, podemos ver que la variable foo
apunta al mismo objeto cuando está en el proc como fuera de él. Podemos actualizarla dentro del proc para que la variable fuera de él se actualice también.
Bindings
Para llevar la cuenta del ámbito actual, Ruby utiliza bindings, que encapsulan el contexto de ejecución en cada posición del código. El método binding
devuelve un objeto Binding
que describe los bindings en la posición actual.
1 2 |
foo = 1 binding.local_variables # => |
El objeto binding tiene un método llamado #local_variables
que devuelve los nombres de todas las variables locales disponibles en el ámbito actual.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Se puede evaluar el código en el binding utilizando el método #eval
. El ejemplo anterior no es muy útil, ya que simplemente llamando a foo
tendría el mismo resultado. Sin embargo, dado que un binding es un objeto que se puede pasar, se puede utilizar para algunas cosas más interesantes. Veamos un ejemplo.
Un ejemplo de la vida real
Ahora que hemos aprendido sobre los bindings en la seguridad de nuestro garaje, como llevarlos a las pistas y jugar en la nieve. Aparte del uso interno que hace Ruby de los bindings en todo el lenguaje, hay algunas situaciones en las que los objetos binding se usan explícitamente. Un buen ejemplo es ERB-el sistema de plantillas de Ruby.
1 2 3 4 5 6 7 8 9 10 |
require 'erb' x = 1 def y 2 end template = ERB.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`") template.result(binding) # => "x is 1, y() returns 2, self is `main`" |
En este ejemplo, creamos una variable llamada x
, un método llamado y
, y una plantilla ERB que hace referencia a ambos. A continuación, pasamos la vinculación actual a ERB#result
, que evalúa las etiquetas ERB en la plantilla y devuelve una cadena con las variables rellenadas.
Bajo el capó, ERB utiliza Binding#eval
para evaluar el contenido de cada etiqueta ERB en el ámbito de la vinculación pasada. Una implementación simplificada que funciona para el ejemplo anterior podría ser así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class DiyErb def initialize(template) @template = template end def result(binding) @template.gsub(/<%=(.+?)%>/) do binding.eval() end end end x = 1 def y 2 end template = DiyErb.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`") template.result(binding) # => "x is 1, y() returns 2, self is `main`" |
La clase DiyErb
toma una cadena de plantilla en la inicialización. Su método #result
encuentra todas las etiquetas ERB y las reemplaza con el resultado de evaluar su contenido. Para ello, llama a Binding#eval
sobre el binding pasado, con el contenido de las etiquetas ERB.
Pasando el binding actual al llamar al método #result
, las llamadas a eval
pueden acceder a las variables definidas fuera del método, e incluso fuera de la clase, sin tener que pasarlas explícitamente.
¿Te hemos perdido en el bosque?
Esperamos que hayas disfrutado de nuestro viaje de esquí al bosque. Profundizamos en los alcances y cierres, después de glosarlos. Esperamos no haberte perdido en el bosque. Por favor, haznos saber si quieres aprender más sobre fijaciones, o tienes algún otro tema de Ruby en el que te gustaría sumergirte.
Gracias por seguirnos y, por favor, quita la nieve de tus fijaciones antes de dejarlas para el próximo promotor. Si te gustan estos viajes mágicos, quizá quieras suscribirte a Ruby Magic para recibir un correo electrónico cuando publiquemos un nuevo artículo aproximadamente una vez al mes.