Análisis de Ámbito: Conceptos

El Problema del Análisis de Ámbito

En los lenguajes de programación name binding (binding = encuadernado, encarpetado, ligadura, unificación) es la asociación de valores con identificadores. Decimos de un identificador ligado a un valor que es una referencia a dicho valor. El concepto de binding es un concepto proveído por los lenguajes de programación: a nivel de máquina no existe el concepto de binding, de relación (nombre, valor). El concepto de Binding esta intimamente relacionado con el concepto de ámbito scope), ya que el análisis de ámbito es la determinación de las relaciones de binding.

El problema del análisis de ámbito sería sencillo sino fuera porque los lenguajes de programación suelen permitir el uso del mismo nombre5.1 para denotar distintos elementos de un programa. Es por ello que es necesario determinar que definición o declaración se aplica a una determinada ocurrencia de un elemento.

Definición 5.1.1   En un lenguaje de programación, una declaración es un constructo sintáctico que define y provee información sobre un nombre. La declaración provee información sobre las propiedades asociadas con el uso del nombre: 'este nombre es una función que recibe enteros y retorna enteros', 'este nombre puede ser usado para referirse a listas de enteros y es visible sólo en el ámbito actual', etc.

Definición 5.1.2   Las reglas de ámbito de un lenguaje determinan que declaración del nombre es la que se aplica cuando el nombre es usado.

Binding Estático y Binding Dinámico

En la definición anterior no se especifica en que momento se resuelve la correspondencia (nombre, definición).

Se habla de static binding cuando las reglas y la resolución de la correspondencia (nombre, definición) puede ser resuelta en tiempo de compilación, a partir del análisis del texto del programa fuente (también se habla de early binding).

Por el contrario cuando se habla de dynamic binding cuando la determinación de que definición se aplica a un nombre es establecida en tiempo de ejecución (también se denomina late binding o virtual binding).

Un ejemplo de static binding es una llamada a a una función en C: la función referenciada por un identificador no puede cambiarse en tiempo de ejecución. Un ejemplo de binding dinámico puede ocurrir cuando se trabaja con métodos polimorfos en un lenguaje de programación orientada a objetos, ya que la definición completa del tipo del objeto no se conoce hasta el momento de la ejecución.

El siguiente ejemplo de Dynamic_binding,tomado de la wikipedia, ilustra el binding dinámico.

Supongamos que todas las formas de vida son mortales. En OOP podemos decir que la clase Persona y la clase Planta deben implementar los métodos de Mortal, el cual contiene el método muere.

Las personas y las plantas mueren de forma diferente, por ejemplo las plantas no tienen un corazón que se detenga. Dynamic binding es la práctica de determinar que definición/declaración se aplica a un método en tiempo de ejecución:

void mata(Mortal m) {
  m.muere();
}

No esta claro cual es la clase actual de m, una persona o una planta. Ambos Planta.muere y Persona.muere pueden ser invocados. Cuando se usa dynamic binding, el objeto m es examinado en tiempo de ejecución para determinar que método es invocado. Esto supone una 'renuncia' por parte del lenguaje y su compilador a obtener una definición completa del objeto.

Definición 5.1.3   Cuando se usa static binding, la parte del texto del programa al cual se aplica la declaración de un nombre se denomina ámbito de la declaración

Ejercicio 5.1.1   En el siguiente código existen dos definiciones para el nombre one, una en la línea 11 y otra en la línea 20.

pl@europa:~/src/perl/perltesting$ cat -n ds.pl
     1  package Father;
     2  use warnings;
     3  use strict;
     4
     5  sub new {
     6    my $class = shift;
     7
     8    bless { @_ }, $class;
     9  }
    10
    11  sub one {
    12    "Executing Father::one\n";
    13  }
    14
    15  package Child;
    16  use warnings;
    17  use strict;
    18  our @ISA = 'Father';
    19
    20  sub one {
    21    "Executing Child::one\n";
    22  }
    23
    24  package main;
    25
    26  for (1..10) {
    27    my $class = int(rand(2)) ? 'Child' : 'Father';
    28    my $c = $class->new;
    29    print $c->one;
    30  }
¿Que definiciones se aplican a los 10 usos del nombre one en la línea 28? ¿Estos usos constituyen un ejemplo de binding estático o dinámico? ¿Cuál es el ámbito de las declaraciones de one?

Incluso en los casos en los que la resolución del binding se deja para el momento de la ejecución el compilador debe tener información suficiente para poder generar código. En el caso anterior, el compilador de Perl infiere de la presencia de la flecha en $c->one que one es el nombre de una subrutina.

Ejercicio 5.1.2   En el siguiente ejemplo se usa una referencia simbólica para acceder a una función:
pl@europa:~/src/perl/testing$ cat -n symbolic.pl
 1  use warnings;
 2  use strict;
 3
 4  sub one {
 5    "1\n";
 6  }
 7
 8  sub two {
 9    "2\n";
10  }
11
12  my $x = <>;
13  chomp($x);
14
15  no strict 'refs';
16  print &$x();
Al ejecutarlo con entrada one obtenenmos:
pl@europa:~/src/perl/testing$ perl symbolic.pl
one
1
¿El uso de la línea 16 es un ejemplo de binding estático o dinámico? ¿Cuál es el binding de las declaraciones de x?

Ejercicio 5.1.3   En el siguiente ejemplo la clase Two hereda de One.
pl@europa:~/src/perl/testing$ cat -n latebinding.pl
     1  package One;
     2  use warnings;
     3  use strict;
     4
     5  our $x = 1;
     6  sub tutu {
     7    "Inside tutu: x = $x\n";
     8  }
     9
    10  package Two;
    11  use warnings;
    12  use strict;
    13  our @ISA = 'One';
    14
    15  our $x = 2;
    16
    17  print Two->tutu();
¿Qué definición de $x se aplica al uso en la línea 7? ¿Cuál será la salida del programa?

Definición 5.1.4   La tarea de asignar las ocurrencias de las declaraciones de nombres a las ocurrencias de uso de los nombres de acuerdo a las reglas de ámbito del lenguaje se denomina identificación de los nombres

Definición 5.1.5   Una ocurrencia de un nombre se dice local si está en el ámbito de una declaración que no se aplica desde el comienzo de la declaración hasta el final del texto del programa. Tal declaración es una declaración local.

Definición 5.1.6   Si, por el contrario, una ocurrencia de un nombre está en el ámbito de una declaración que se aplica desde el comienzo de la declaración hasta el final del texto del programa se dice que la declaración es una declaración global.

Definición 5.1.7   Aunque la definición anterior establece el atributo ámbito como un atributo de la declaración es usual y conveniente hablar del
"ámbito del nombre x"
como una abreviación de
"el ámbito de la declaración del nombre x que se aplica a esta ocurrencia de x"

Intervención del Programador en Tiempo de Compilación

En algunos lenguajes - especialmente en los lenguajes dinámicos- la diferenciación entre tiempo de compilación y tiempo de ejecución puede ser difusa. En el siguiente fragmento de código Perl se usa el módulo Contextual::Return para crear una variable cuya definición cambia con la forma de uso.

lhp@nereida:~/Lperl/src/testing$ cat -n context1.pl
 1  #!/usr/local/bin/perl -w
 2  use strict;
 3  use Contextual::Return;
 4
 5  my $x = BOOL { 0 } NUM { 3.14 } STR { "pi" };
 6
 7  unless ($x) { warn "¡El famoso número $x (".(0+$x).") pasa a ser falso!\n" } # executed!

lhp@nereida:~/Lperl/src/testing$ context1.pl
¡El famoso número pi (3.14) pasa a ser falso!

Obsérvese que el binding de $x es estático y que a los tres usos de $x en la línea 7 se les asigna la definición en la línea 5. La declaración de $x ocurre en lo que Perl denomina 'tiempo de compilación', sin embargo, el hecho de que un módulo cargado en tiempo de compilación puede ejecutar sentencias permite a Contextual::Return expandir el lenguaje de las declaraciones Perl.

Ejercicio 5.1.4   Considere el siguiente código Perl:
pl@europa:~/src/perl/testing$ cat -n contextual.pl
     1  #!/usr/bin/perl -w
     2  use strict;
     3  use Contextual::Return;
     4
     5  sub sensible {
     6    return STR { "one" }
     7           NUM { 1 }
     8           LIST   { 1,2,3 }
     9           HASHREF   { {name => 'foo', value => 99} }
    10    ;
    11  }
    12
    13  print "Result = ".sensible()."\n";
    14  print "Result = ".(0+sensible())."\n";
    15  print "Result = ",sensible(),"\n";
    16  print "Result = (name = ",sensible()->{name},", value = ", sensible()->{value},")\n";
Cuando se ejecuta, este programa produce la siguiente salida:
pl@europa:~/src/perl/testing$ ./contextual.pl
Result = one
Result = 1
Result = 123
Result = (name = foo, value = 99)

Las relaciones de definición-uso de la función sensible ¿Son un caso de binding estático o de binding dinámico? ¿Cuál es el ámbito de la declaración de sensible en las líneas 5-11?

Visibilidad

Como ya sabemos, es falso que en el ámbito de una declaración que define a x dicha declaración se aplique a todas las ocurrencias de x en su ámbito. En un ámbito estático, una declaración local a la anterior puede ocultar la visibilidad de la declaración anterior de x.

Definición 5.1.8   Las reglas de visibilidad de un lenguaje especifican como se relacionan los nombres con las declaraciones que se les aplican.

El concepto de nombre depende del lenguaje. Algunos lenguajes permiten que en un cierto ámbito haya mas de una definición asociada con un identificador. Un mecanismo que puede ser usado para determinar univocamente que definición se aplica a un determinado uso de un nombre es que el uso del nombre vaya acompañado de un sigil. (podría reinterpretarse que en realidad el nombre de una variable en Perl incluye el sigil).

Así, en Perl tenemos que es legal tener diferentes variables con nombre x: $x, @x, %x, &x, *x, etc. ya que van prefijadas por diferentes sigils $, @, etc. El sigil que prefija x determina que definición se aplica al uso de x (La palabra sigil hace referencia a 'sellos mágicos' - combinaciones de símbolos y figuras geométricas - que son usados en algunas invocaciones con el propósito de producir un sortilegio).

En algunos casos existen mecanismos para la extensión de los nombres. En Perl es posible acceder a una variable de paquete ocultada por una léxica usando su nombre completo.

La asignación de una declaración a ciertos usos de un identificador puede requerir de otras fases de análisis semántico, como el análisis de tipos. El uso de los nombres de campo de un registro en Pascal y en C constituye un ejemplo:

 1	type 
 2	  a = ^b;
 3	  b = record
 4	        a: Integer;
 5	        b: Char;
 6	        c: a
 7	      end;
 8	var
 9	  pointertob: a;
10	  c         : Integer;
11	
12	...
13	new(pointertob);
14	
15	pointertob^.c := nil;
16	c             := 4;
17	...

El uso de c en la ĺinea 15 es posible porque el tipo de la expresión pointertob^ es un registro. La definición que se aplica al uso de c en la línea 16 es la de la línea 10.

También es posible hacer visible un nombre escondido - sin necesidad de extender el identificador - mediante alguna directiva que lo haga visible: Un ejemplo es la declaración with the Pascal:

new(pointertob);
 
with pointertob^ do
begin
  a := 10;
  b := 'A';
  c := nil
end;
...

Declaraciones y Definiciones

En algunos lenguajes se distingue entre declaraciones que sólo proporcionan información sobre el elemento y lo hacen visible - pero no asignan memoria o producen código para la implementación del mismo - y otras que si producen dicho código. A las primeras se las suele llamar declaraciones y a las segundas definiciones. En tales lenguajes se considera un error que dos declaraciones de un mismo elemento difieran.

Por ejemplo, en C una variable o función sólo es definida una vez, pero puede ser declarada varias veces. El calificativo extern es usado en C para indicar que una declaración provee visibilidad pero no conlleva definición (creación):

extern char stack[10];
extern int stkptr;

Estas declaraciones le dicen al compilador C que las definiciones de los nombres stack y stackptr se encuentran en otro fichero. Si la palabra extern fuera omitida el compilador asignaría memoria para las mismas.

Otro ejemplo en el que una directiva hace visible una definición escondida es el uso de la declaración our de Perl cuando un paquete está repartido entre varios ficheros que usan repetitivamente strict:

Ejercicio 5.1.5   Considere el siguiente programa:
pl@europa:~/src/perl/perltesting$ cat -n useA.pl
     1  #!/usr/bin/perl
     2  package A;
     3  use warnings;
     4  use strict;
     5
     6  use A;
     7
     8  #our $x;
     9  print "$x\n";
La variable $x esta declarada en el fichero A.pm:
pl@europa:~/src/perl/perltesting$ cat -n A.pm
     1  package A;
     2  use warnings;
     3  use strict;
     4
     5  our $x = 1;
     6
     7  1;

Sin embargo la compilación de useA.pl produce errores, pues $x no es visible:

pl@europa:~/src/perl/perltesting$ perl -c useA.pl
Variable "$x" is not imported at useA.pl line 9.
Global symbol "$x" requires explicit package name at useA.pl line 9.
useA.pl had compilation errors.
El mensaje se arregla descomentando la declaración de $x en la línea 8 de useA.pl:
pl@europa:~/src/perl/perltesting$ perl -ce `sed -e 's/#our/our/' useA.pl`
-e syntax OK
La declaración de la línea 8 hace visible la variable $x en el fichero useA.pl.

¿Cual es entonces el ámbito de la declaración de $x en la línea 5 de A.pm? ¿Es todo el paquete? ¿O sólo el segmento del paquete que está en el fichero A.pm? (se supone que trabajamos con strict activado).

Inferencia, Declaraciones Implícitas y Ámbito

Ejercicio 5.1.6   Tanto en los lenguajes estáticos como en los dinámicos se suele requerir que exista una declaración del objeto usado que determine las propiedades del mismo.

Aunque en los lenguajes dinámicos la creación/definición del elemento asociado con un nombre puede postergarse hasta el tiempo de ejecución, la generación de código para la sentencia de uso suele requerir un conocimiento (aunque sea mínimo) del objeto que esta siendo usado. En algunos casos, es necesario inferir la declaración a partir del uso, de manera que la declaración asociada con un uso es construida a partir del propio uso.

Los lenguajes típicamente estáticos fuertemente tipeados suelen requerir que para todo uso exista una declaración explícita y completa del nombre y de las operaciones que son válidas sobre el mismo al finalizar la fase de compilación. Sin embargo, el código para la creación/definición de algunos objetos puede ser postergado a la fase de enlace. De hecho, la resolución de ciertos enlaces pueden ocurrir durante la fase de ejecución (énlace dinámico).

El siguiente ejemplo hace uso de un typeglob selectivo en la línea 8 para definir la función ONE:

pl@europa:~/src/perl/testing$ cat -n glob.pl
     1  use warnings;
     2  use strict;
     3
     4  sub one {
     5    "1\n"
     6  }
     7
     8  *ONE = \&one;
     9  print ONE();
Al ejecutar este programa se produce la salida;
pl@europa:~/src/perl/testing$ perl glob.pl
1
¿El uso de ONE en la línea 9 es un ejemplo de binding estático o dinámico? ¿Cuál es el ámbito de la declaración de ONE?

Responda estas mismas preguntas para esta otra variante del ejemplo anterior:

pl@nereida:~/src/perl/perltesting$ cat -n coderef.pl
     1  use warnings;
     2  use strict;
     3
     4  *ONE = sub { "1\n" };
     5  print ONE();

En los ejemplos anteriores el propio uso del nombre ONE actúa como una declaración: Perl deduce de la presencia de paréntesis después de ONE que ONE es el nombre de una función. Esta información es suficiente para generar el código necesario. Podría decirse que la forma del uso declara al ente ONE y que la línea de uso conlleva una declaración implícita. Sin embargo, la creación/definición completa de ONE es postergada hasta la fase de ejecución.

Ejercicio 5.1.7   La conducta del compilador de Perl cambia si se sustituye el programa anterior por este otro:
pl@europa:~/src/perl/testing$ cat -n globwarn.pl
     1  use warnings;
     2  use strict;
     3
     4  sub one {
     5    "1\n"
     6  }
     7
     8  *ONE = \&one;
     9  my $x = ONE;
    10  print $x;
Al compilar se obtiene un error:
pl@europa:~/src/perl/testing$ perl -c globwarn.pl
Bareword "ONE" not allowed while "strict subs" in use at globwarn.pl line 9.
globwarn.pl had compilation errors.
¿Sabría explicar la causa de este cambio de conducta?

Ejercicio 5.1.8   El error que se observa en el ejercicio anterior desaparece cuando se modifica el código como sigue:
lusasoft@LusaSoft:~/src/perl/perltesting$ cat -n globheader.pl
     1  #!/usr/bin/perl
     2  use warnings;
     3  use strict;
     4
     5  sub ONE;
     6
     7  sub one {
     8    "1\n"
     9  }
    10
    11  *ONE = \&one;
    12  my $x = ONE;
    13  print $x;
¿Cual es el significado de la línea 5?

En el caso del lenguaje Simple C introducido en la práctica 5.3 hay una única declaración que se aplica a cada ocurrencia correcta de un nombre en el ámbito de dicha declaración.

Esto no tiene porque ser siempre así: en ciertos lenguajes una redeclaración de un cierto nombre x puede que sólo oculte a otra declaración previa de x si las dos declaraciones asignan el mismo tipo a x. Esta idea suele conocerse como sobrecarga de identificadores. De todos modos, sigue siendo cierto que para que el programa sea considerado correcto es necesario que sea posible inferir para cada ocurrencia de un identificador que única definición se aplica. Así una llamada a una cierta función min(x,y) llamaría a diferentes funciones min según fueran los tipos de x e y. Para resolver este caso es necesario combinar las fases de análisis de ámbito y de análisis de tipos.

Algunos lenguajes - especialmente los lenguajes funcionales - logran eliminar la mayoría de las declaraciones. Disponen de un mecanismo de inferencia que les permite - en tiempo de compilación - deducir del uso la definición y propiedades del nombre.

Véase como ejemplo de inferencia la siguiente sesión en OCaml

pl@nereida:~/src/perl/attributegrammar/Language-AttributeGrammar-0.08/examples$ ocaml
        Objective Caml version 3.09.2

# let minimo = fun i j -> if i<j then i else j;;
val minimo : 'a -> 'a -> 'a = <fun>
# minimo 2 3;;
- : int = 2
# minimo 4.9 5.3;;
- : float = 4.9
# minimo "hola" "mundo";;
- : string = "hola"

El compilador OCaml infiere el tipo de las expresiones. Así el tipo asociado con la función minimo es

                    'a -> 'a -> 'a

que es una expresión de tipo que contiene variables de tipo. El operador -> es asociativo a derechas y asi la expresión debe ser leída como 'a -> ('a -> 'a). Básicamente dice:

El tipo de la expresión es una función que toma un argumento de tipo 'a (donde 'a es una variable tipo que será instanciada en el momento del uso de la función) y devuelve una función que toma elementos de tipo 'a y retorna elementos de tipo 'a.

Ámbito Dinámico

En el ámbito dinámico, cada nombre para el que se usa ámbito dinámico tiene asociada una pila de bindings. Cuando se crea un nuevo ámbito dinámico se empuja en la pila el viejo valor (que podría no estar definido). Cuando se sale del ámbito se saca de la pila el antiguo valor. La evaluación de x retorna siempre el valor en el top de la pila.

La sentencia local de Perl provee de ámbito dinámico a las variables de paquete. Una aproximación a lo que ocurre cuando se ejecuta local es:



DECLARACIÓN DE local SIGNIFICADO
{
  local($SomeVar);
  $SomeVar = 'My Value';
       ...
}
{
  my $TempCopy = $SomeVar;
  $SomeVar = undef;
  $SomeVar = 'My Value';
       ...
  $SomeVar = $TempCopy;
}


La diferencia entre ámbito dinámico y estático debería quedar mas clara observando la conducta del siguiente código

lhp@nereida:~/Lperl/src$ cat -n local.pl
 1  #!/usr/bin/perl -w
 2  use strict;
 3
 4  our $x;
 5
 6  sub pr { print "$x\n"; }
 7  sub titi { my $x = "titi"; pr(); }
 8  sub toto { local $x = "toto"; &pr(); &titi(); }
 9
10  $x = "global";
11  &pr();
12  &toto();
13  &titi();

Cuando se ejecuta, se obtiene la siguiente salida:

> local.pl
global
toto
toto
global

Ejercicio 5.1.9   ¿Es local una declaración o una sentencia? ¿Que declaraciones se aplican a los diferentes usos de $x en las líneas 6, 7, 8 y 10?.



Subsecciones
Casiano Rodríguez León
2012-01-30