Sunteți pe pagina 1din 23

Historia

La programacin imperativa es mucho ms cercana a la arquitectura fsica del


computador que la programacin declarativa. La arquitectura que
habitualmente se utiliza en los computadores y que es la base de la gran
mayora de lenguajes de programacin es la arquitectura tradicional propuesta
por Von Newmann. En esta arquitectura los datos se almacenan en una
memoria a la que se accede desde una unidad de control ejecutando
instrucciones de forma secuencial.
El estilo de programacin imperativo constituye la primera forma de programar
ordenadores. El ensamblador y el cdigo mquina que se utilizaban antes de
desarrollarse los primeros lenguajes de programacin tienen un enfoque
totalmente imperativo. Los primeros lenguajes de programacin de alto nivel
(como el Fortran) eran abstracciones del lenguaje ensamblandor y mantenan
este enfoque. Lenguajes ms modernos como el BASIC o el C han continuado
esta idea.
En los aos 60 se introducen conceptos de programacin procedural en los
lenguajes de programacin. La programacin procedural es un tipo de
programacin imperativa en el que los programas se descomponen
procedimientos (tambin llamados subrutinas o funciones). Los cambios de
estado se localizan en estos procedimientos y se restringen a valores pasados
como parmetros o a los valores devueltos por los procedimientos.
A finales de la dcada de los 60 Edsger W. Dijkstra, una de las figuras ms
importantes en la historia de la computacin, public en la revista
Communications of the ACM el importante artculo GOTO statement considered
harmful en el que propone que la sentencia GOTO se elimine de los futuros
lenguajes de programacin. Este artculo marca el inicio de una nueva
tendencia de programacin denominada programacin estructurada que,
manteniendo la programacin imperativa, intenta conseguir lenguajes que
promuevan programas correctos, modulares y mantenibles. Lenguajes
representativos de la programacin estructurada son el Pascal, el ALGOL 68 o
el Ada.
A finales de la dcada de los 70 la programacin orientada a objetos extiende
estos conceptos y, siguiendo con el enfoque imperativo, introduce otras
caractersticas ms avanzadas.
Caractersticas principales
Ya hablamos de la programacin imperativa cuando la comparbamos al
principio de curso con la programacin declarativa. En programacin
imperativa la computacin se realiza cambiando el estado del programa por
medio de sentencias que definen pasos de ejecucin. Las dos caractersticas

principales del paradigma imperativo son, por tanto, la existencia de estado


modificable y la ejecucin de sentencias de control del programa.
Estado de un programa
En la programacin imperativa el estado del programa se mantiene en forma
de datos en la memoria del computador. Estos datos son modificables
mediante sentencias de asignacin.
Modificacin de datos
Uno de los elementos de la arquitectura de Von Newmman son las celdas de
memoria en la que se almacenan los datos. Estas celdas de memoria tienen
direcciones nicas y pueden modificarse con sentencias especficas. Una tarea
fundamental de un lenguaje imperativo es proporcionar una abstraccin que
convierta estas celdas de memoria en conceptos de ms alto nivel, en forma
de datos accesibles y modificables.
Un Array, por ejemplo, define una estructura de datos que se almacena
directamente en memoria y que puede ser accedido y modificado.
En Scala podemos definir un array utilizando la palabra clave Array. Los arrays
de Scala, al igual que las listas, son homogneos. Esto es, todos sus objetos
deben tener el mismo tipo de datos. A diferencia de las listas, los arrays son
mutables, aunque de tamao fijo. Una vez instanciada una variable de tipo
array podemos modificar sus componentes pero no aadir ms elementos.
val unoDosTres = Array("uno","dos","tres")

El cdigo anterior define implcitamente la variable unoDosTres como una variable de tipo
Array[String]. Para acceder a los componentes de un array se utilizan los parntesis y se
comienza por el cero. Por ejemplo, el siguiente cdigo modifica el valor de la ltima
componente del array con la primera:
unoDosTres(2) = unoDosTres(0)

Y la siguiente funcin muestra cmo se define un parmetro de tipo array de cadenas


(Array[String]). La funcin recibe un array y copia en todos sus elementos el primer
dato.
def llenaArray(array: Array[String]) = {
var i = 1
while (i < array.length) {
array(i) = array(0)
i += 1
}
}

Almacenamiento de datos en variables: valor y referencia

Todos los lenguajes de programacin imperativos definen variables que se encargan de


almacenar o referenciar el estado del programa. Dependiendo de si se el tipo de la variable
almacena valores o referencias hablamos de tipos de valor (value types) y tipos de
referencia (reference types).
Por ejemplo, en el caso de C o C++ los tipos de datos primitivos como int o char son de
tipo valor. Cuando una variable se asigna a otra, o cuando el dato se pasa como parmetro,
se crea una nueva copia que se almacena en la nueva variable o se pasa como parmetro.
En el caso de Scala, aunque todos los tipos de datos son objetos (de tipo referencia), la
semntica de copia de valor se utiliza para los tipos de datos simples.
var x = 10
var y = x
x = 20
y

Sin embargo, no sucede lo mismo con los tipos compuestos. En la mayora de lenguajes de
programacin imperativos (como C, C++, Java, Python o Scala) estos tipos tienen una
semntica de referencia. Cuando declaramos una variable de un tipo compuesto se
almacena una referencia. Y cuando se hace una asignacin se copia la referencia. Sucede
esto, por ejemplo, en el caso de Arrays, de Strings o, ms general todava, en el caso de
objetos en Programacin Orientada a Objetos. El concepto de referencia es propio de la
programacin imperativa. Los datos se encuentran en la memoria del computador y las
variables guardan una referencia a ellos.
En este paradigma, distintas variables pueden guardar la misma referencia, de forma que
cuando se modifican los datos se est modificando de forma lateral los valores de todas las
variables que los referencian.
El hecho de que ms de una variable puede contener la misma referencia permite construir
estructuras de datos muy eficientes que pueden ser actualizadas con gran rapidez (slo hay
que modificar el valor en un sitio). Pero hay que ser cuidadoso en el manejo de estas
estructuras por la posibilidad de producir efectos laterales no deseados.
var x = Array(1,2,3,4)
var y = x
x(0) = 10
x
y

La funcin anterior llenaArray recibe un tipo de referencia como parmetro (un array de
cadenas) y modifica la referencia que se pasa como parmetro.
Igualdad de valor y de referencia
Los lenguajes imperativos en los que se definen referencias cuyos valores pueden ser
modificados necesitan definir dos tipos de igualdad: igualdad de valor y de referencia.

Dos variables son iguales en valor cuando contienen los mismos valores,
independientemente de si se encuentran en la misma direccin. Dos variables son iguales en
referencia cuando apuntan a un mismo objeto o dato. Unas variables pueden ser iguales en
valor, pero no en referencia. Si dos variables son iguales en referencia tambin lo sern en
valor.
Por ejemplo, el lenguaje de programacin Java define el operador "==" como un operador
de igualdad de referencia y el mtodo equals en la clase Object (padre de todas las dems
clases) como un mtodo que hay que redefinir en las clases para la igualdad de valor.

Sentencias de control
La otra caracterstica fundamental de la programacin imperativa tiene tambin su origen
en la arquitectura de Von Newman. Se trata de la ejecucin de pasos elementales en los que
el control va modificando el contador de programa que indica la siguiente instruccin a
ejecutar.
Las sentencias de control de los lenguajes imperativos que utilizan la programacin
estructurada se pueden agrupar en: secuencia, seleccin e iteracin:

Las sentencias de secuencia definen instrucciones que son ejecutados una detrs de
otra de forma sncrona. Una instruccin no comienza hasta que la anterior ha
terminado.

Las sentencias de seleccin definen una o ms condiciones que determinan las


instrucciones que se debern ejecutar.

Las sentencias de iteracin definen instrucciones que se ejecutan de forma repetitiva


hasta que se cumple una determinada condicin.

Adems, en la programacin imperativa tambin debemos entender una llamada a un


procedimiento o una funcin como una sentencia de control. En ella se modifica el
contador del programa y se pasan a ejecutar las sentencias definidas en el procedimiento.
El modelo de evaluacin de la programacin imperativa ya no es el modelo de sustitucin,
sino un modelo basado en la ejecucin de sentencias y la modificacin de los datos
almacenados.

Datos mutables
Mutadores en Scheme
(set! <simbolo> <expresion>)

(set-car! <pareja> <expresion>)


(set-cdr! <pareja> <expresion>)

Un ejemplo de mutacin y efecto lateral con una pareja:


(define a (cons 1 2))
(define b a)
(set-car a 10)
a
b

La introduccin de la mutacin nos obliga a diferenciar entre igualdad de valor e igualdad


de contenido:

La igualdad de referencia se comprueba con la funcin eq?: (eq? x y) devuelve #t


cuando x e y apuntan al mismo dato

La igualdad de contenido se comprueba con la funcin equal?: (equal? x y)


devuelve #t cuando x e y contienen el mismo valor

Si dos variables son eq? tambin son equal?.


(define a
(define b
(define c
(equal? a
(equal? a
(eq? a b)
(eq? a c)

(cons 1 2))
(cons 1 2))
a)
b)
c)

Una ventaja de los operadores de mutacin es la actualizacin eficiente de estructuras de


datos. Por ejemplo, una insercin en una lista ordenada en Scheme:
(define (make-olist)
(list '*list*))
(define (insert! n olist)
(cond
((null? (cdr olist)) (set-cdr! olist (cons n '())))
((< n (cadr olist)) (set-cdr! olist (cons n (cdr olist))))
((= n (cadr olist)) #f) ; el valor devuelto no importa
(else (insert! n (cdr olist)))))

Podemos probar la funcin anterior:


(define a (make-olist))
(insert! 15 a)
a
(insert! 1 a)
a

Se necesita aadir una cabecera a la lista, porque si no lo hiciramos y quisramos insertar


un elemento en la primera posicin de la lista, perdieramos su referencia. Aadiendo una

cabecera todos los elementos se insertarn a continuacin de ella, por lo que esta cabecera
funcionar como un elemento inmutable al que apuntar para referenciar la lista.
Otro ejemplo: una tabla hash definida con una lista de asociacin.
(define (make-table)
(list '*table*))
(define (get key table)
(let ((record (assq key (cdr table))))
(if (not record)
#f
(cdr record))))
(define (put key value table)
(let ((record (assq key (cdr table))))
(if (not record)
(set-cdr! table
(cons (cons key value) (cdr table)))
(set-cdr! record value))))

Datos mutables en Scala


Por defecto, los objetos de cualquier clase en Scala son mutables. Lo veremos en
programacin orientada a objetos.
Scala tiene una gran cantidad de colecciones mutables: http://www.scalalang.org/api/current/scala/collection/mutable/package.html
Vamos a comentar la clase ListBuffer, que implementa listas mutables a las que se
pueden aadir elementos por el principio y por el final en tiempo constante. La lista
completa de mtodos se puede consultar en el API de Scala.
import scala.collection.mutable.ListBuffer
val buf = new ListBuffer[Int]
buf += 1
buf += 2
3 +: buf

Son tipos de referencia. Las siguientes instrucciones asigna la referencia de buf a buf2.
Cuando modificamos buf2 tambin estamos modificando buf:
val buf = new ListBuffer[Int]
val buf2 = buf
buf2 += 4

La funcin ++= aade a una lista el contenido de otra coleccin:


val buf = new ListBuffer[Int]
buf += 1
val buf2 = new ListBuffer[Int]
buf2 ++= List(2,3,4)
buf ++= buf2

La funcin -= elimina un elemento de la lista (slo uno, el primero que encuentra):


val buf = new ListBuffer[String]
buf ++= List("Paris","Madrid","Londres")
buf -= "Paris"

La funcin indexOf devuelve la posicin de un dato.


val buf = new ListBuffer[String]
buf ++= List("Paris","Madrid","Londres")
buf.indexOf("Madrid")

La funcin update modifica el dato situado en una determinada posicin de la lista.


val buf = new ListBuffer[String]
buf ++= List("Paris","Madrid","Londres")
buf.update(1,"Barcelona")

Estructuras de control
Sentencias de secuencia
En Scheme es posible definir en el cuerpo de una funcin o de un let ms de una
expresin. En este caso todas las expresiones se ejecutan secuencialmente y se devuelve el
resultado de la ltima expresin. En Scala funciona igual.
(define x 3)
(define y 5)
(define (cambia-vars a b)
(set! x a)
(set! y (+ a b x))
(+ x y))

Interesante en el ejemplo: mbito de variables x e y y pasos de ejecucin.


El mismo ejemplo en Scala:
var x=3
var y=5
def cambiaVars(a: Int, b: Int) = {
x=a
y=a+b+x
x+y }

Adems, en Scheme tenemos la forma especial begin que permite ejecutar expresiones
como pasos de ejecucin en aquellos lugar. Tambin es posible definir un cuerpo de una
funcin o de un let con ms de una expresin que tambin se ejecutan secuencialmente. Se
devuelve el valor de la ltima expresin. Un ejemplo:

Sentencias de seleccin
if
Una caracterstica especial de la sentencia if de Scala es que devuelve un valor.
val filename = if (!args.isEmpty) args(0) else "default.txt"
println(filename)

match
La sentencia match permite evaluar una variable o expresin y comparar el resultado con
un conjunto de opciones. Un ejemplo:
def pruebaMatch(str: String) = {
str match {
case "salt" => println("pepper")
case "chips" => println("salsa")
case "eggs" => println("bacon")
case _ => println("huh?")
}
}

Al igual que la sentencia if, la sentencia devuelve el ltimo valor que evalua:
def pruebaMatch2(str: String): String = {
str match {
case "salt" => "pepper"
case "chips" => "salsa"
case "eggs" => "bacon"
case _ => "huh?"
}
}

Sentencias de repeticin
Aunque Scheme tambin tiene bucles, vamos a centrarnos en Scala.
while
def gcdLoop(x: Long, y: Long): Long = {
var a = x
var b = y
while (a != 0) {
val temp = a
a = b % a
b = temp
}
b
}
do-while
def leerEntrada() = {

var line = ""


do {
line = readLine()
println("Read: " + line)
} while (line != "")
}

Expresiones for en Scala


Iterando por colecciones:
def printFiles() = {
val filesHere: Array[java.io.File] = (new java.io.File(".")).listFiles
for (file <- filesHere)
println(file)
}

Bucles for con contador. Interesante notar: creacin de array con un nmero de elementos.
def numsCuadrados(n: Int): (Array[Int],Array[Int]) = {
val nums = new Array[Int](n)
val cuadrados = new Array[Int](n)
for (i <- 0 until n) {
nums(i) = i
cuadrados(i) = i*i
}
(nums,cuadrados)
}

Bucles anidados:
def divisorPattern(n: Int) = {
for (i <- 1 to n) {
for (j <- 1 to n) {
if ((i % j == 0) || (j % i) == 0)
print("* ")
else
print(" ")
}
println(i);
}
}

Filtrado colecciones

mbito de variables y modelo de entornos


El modelo de sustitucin visto en la programacin funcional no es suficiente para explicar
la semntica de la programacin imperativa. Necesitamos un modelo en el que las variables
mantengan datos o referencias que puedan ser modificados mediante la asignacin.
El modelo de entornos que vamos a explicar es aplicable tanto a Scheme como a Scala. De
hecho, presentaremos todos los ejemplos del apartado ambos lenguajes.

Modelo de entornos
Las variables se guardan en un entorno. Por defecto existe un entorno global en el que se
crean las variables cuando ejecutamos setencias en el intrprete. Veremos que dentro de
este entorno global se van a crear entornos locales, por lo que es importante saber en cada
momento en qu entorno nos encontramos. Las sentencias del programa se ejecutan en un
entorno dado (el global o algn entorno local). Dependendiendo de en qu entorno se
ejecute una sentencia, las variables que aparecen en ellas tendrn un valor u otro.
Se crea una variable en un entorno cuando se ejecuta una sentencia define en Scheme o se
declara una variable con var o val en Scala. Las variables cambian de valor con sentencias
de asignacin y se evalan a su ltimo valor asignado.
Para representar grficamente los entornos Dibujaremos un entorno como un rectngulo
que contiene variables. Las variables las asociaremos con su valor con ":". Cuando el tipo
que se asocia a una variable es un tipo de referencia mutable lo indicaremos con una flecha.
Por ejemplo, supongamos las siguientes sentencias.
En Scheme:
(define
(define
(set! x
(set! y
(define
(define

x 1)
y (+ x 2))
5)
(+ x y))
a (cons 1 2))
b a)

En Scala:
var x=1
var y=x+2
x=5
y=x+y
var a = new ListBuffer[Int]
a ++= List(1,2)
var b = a

El entorno resultante de estas expresiones se muestra en la siguiente figura. Vemos que las
variables x e y han modificado su valor y que las variables a y b referencian ambas el
mismo objeto mutable. Una modificacin en ese dato afectara a ambas variables.

mbitos e invocacin de funciones


Vamos a avanzar ms en el modelo de entornos, aadiendo los entornos locales. Cundo se
crea un entorno local? En Scheme y Scala cuando se invoca una funcin. Todas las
sentencias de la funcin se ejecutan un entorno local creado en el momento de la
invocacin.
Veamos el siguiente ejemplo.
En Scheme:
(define x 0)
(define z 100)
(define (crea-vars)
(define x 10)
(define y (+ x 20))
(+ x y z))
(crea-vars 10)
x
y

En Scala:
var x=0
var z=100
def creaVars() = {
var x=10
var y=x+20
x+y+z
}
creaVars()
x
y

Estamos definiendo una funcin creaVars que crea las variables locales x e y, les asigna
valor y devuelve su suma ms el valor de una variable z definida en el entorno global. Es
accesible el valor de z desde el entorno local? Podemos comprobar que s. Se modifican el
valor de x del entorno global al modificarlo en el entorno local? Comprobamos que no. Y
que tampoco toma valor en el entorno global la variable y creada en el local.
La representacin grfica de los entornos es la siguiente. Vemos a la derecha de cada
entorno las sentencias ejecutadas en l. En el entorno global se crean dos variables y se
define la funcin creaVars. Despus se invoca la funcin. Se crea entonces un entorno
local dentro del global en el que se ejecutan las sentecias de la funcin creaVars: se crea la
variable local x, la variable y y se devuelve la expresin x+y+z. Hay que hacer notar que las
variables definidas en el entorno que contiene al entorno local (en este caso el entorno
global) son accesibles desde l. En este caso, la variable z se puede utilizar sin problemas
dentro del entorno local. Se obtiene su valor en el entorno global.

Vamos a explorar esto ltimo un poco ms. Las variables del entorno "padre" (el entorno
contenedor) pueden ser accedidas desde el entorno local. Pero, pueden ser modificadas?
Lo podemos comprobar con el siguiente ejemplo. Creamos la variable x en el entorno
global y una funcin en donde modificamos su valor. Es importante que en la funcin no
definimos x (no usamos var ni val ni define en Scheme), por lo que estamos accediendo a
la variable definida en el entorno global.
var x = 10
def cambiaX(y: Int) = {
x = x+y
x
}
cambiaX(20)
x

En Scheme:
(define x 10)
(define (cambia-x y)
(set! x (+ x y))
x)
(cambia-x 20)
x

Si ejecutamos cualquiera de estos ejemplos podemos comprobar que se modifica el valor de


x en el entorno global.
La representacin grfica es la siguiente:

En el ejemplo definimos tambin el parmetro y en la funcin cambiaX. Cuando invocamos


la funcin le damos el valor 20. Los parmetros son similares a variables creadas en la
funcin. Su nombre se define en el entorno local y se usa su valor en las sentencias de la
funcin. Una diferencia entre Scala y Scheme es que los parmetros en Scheme pueden ser
modificados, pero en Scala no.
Un resumen de lo que hemos aprendido hasta ahora:

La invocacin a una funcin crea un entorno local contenido en el entorno global

Las variables creadas en ese entorno local no son accesibles desde el entorno global

Las variables del entorno global son accesibles desde el entorno local

Vamos a dar un paso ms. Aunque las variables creadas en un entorno local no son
accesibles desde el entorno global, no pasa lo mismo con los objetos. Podemos crear un
objeto de referencia dentro del entorno local y devolverlo como resultado de la funcin. Por
ejemplo, con el siguiente cdigo:
def creaListBuffer() = {
var a = new ListBuffer[String]
a += "Nueva York"
a
}
var b = creaListBuffer()

O un cdigo similar en Scheme:


(define (crea-pareja)
(define a (cons 1 2))
a)
(define b (crea-pareja))

Tanto en el caso de Scala como en el de Scheme, el objeto creado en el entorno local se


devuelve como resultado de la invocacin de la funcin. No se devuelve una copia, sino el
propio objeto creado en el entorno local.
Y ahora llegamos al ltimo y ms importante punto del modelo de entornos Qu sucede si
en el entorno local creamos y devolvemos una funcin? Vamos a verlo en el apartado
siguiente. Adelantamos que estamos definiendo una closure y que la funcin devuelta tiene
acceso a las variables definidas en el entorno local.

Qu sucede con funciones creadas en mbitos locales?


Veamos lo que sucede con lo que se denominan closures, funciones creadas en un mbito
local. La regla de creacin de funciones (closures) en entornos locales es la siguiente:

Regla de creacin de closures: Cuando se crea una funcin annima en un entorno local y
se devuelve como resultado, la funcin queda asociada al entorno local en que se ha creado.
Una posterior invocacin a la funcin annima se ejecutar dentro de este entorno local.
Esto es, si en un entorno local creamos una funcin annima y la devolvemos, cuando la
invocamos posteriormente podr acceder a las variables definidas en el entorno local. De
ah proviene el nombre de closure. Cuando se devuelve una funcin creada en un mbito, la
funcin se queda guardada en el entorno en el que se ha creado. Al devolver la funcin y
asignarla a una variable en el entorno global podemos considerar que se devuelve todo: la
propia funcin y su mbito.
Podemos generalizar este comportamiento con la regla de invocacin de funciones del
modelo de entornos:
Regla de invocacin de funciones: Cuando se invoca a una funcin se crea un entorno
local dentro del entorno en el que la funcin se cre y se ejecutan todas sus sentencias en
este entorno local.
Es importante notar que se trata de una regla general que sirve tanto para funciones creadas
en el entorno global como para funciones creadas en entornos locales. Las funciones que se
denominan closures son estas ltimas.
Veamos un ejemplo de este comportamiento. Definimos en Scheme la funcin makecontador que crea una variable x inicializada a 0 y despus una funcin annima que
incrementar esta x. Una vez definida la funcin, la invocamos y asignamos su resultado
(una funcin annima) a la variable f. Por ltimo, invocamos un par de veces a f y
consultamos el valor de x en el entorno global:
(define (make-contador)
(define x 0)
(lambda ()
(set! x (+ x 1))
x))
(define f (make-contador))
(f)
(f)
x

En el ejemplo podemos comprobar que cada invocacin a f va incrementando el valor de la


variable local x. Esta variable es el estado local de la funcin, que se modifica en cada
invocacin. La invocacin a f se ejecuta dentro del entorno en el que f se cre (el mismo
en el que est definido x), por lo que se tiene acceso a esta variable.
Vemos que hay dos elementos nuevos que no existan en el paradigma funcional:

Estado local: un conjunto de valores no accesibles desde el entorno global, que


persisten durante el tiempo de ejecucin del programa y a los que es posible acceder
desde ciertas funciones (closures en nuestro caso).

Funciones que devuelven distintos valores: la llamada a la funcin f devuelve un


valor distinto en cada invocacin. Recordemos que esto es incompatible con el
paradigma funcional, en el que una funcin siempre debe devolver el mismo
resultado si se invoca con los mismos parmetros.

El ejemplo anterior se puede programar tambin en Scala de una forma similar:


def makeContador() = {
var x = 0
() => {
x = x+1
x
}
}
f = makeContador()
f()
f()
x

La representacin de los mbitos creados es la siguiente:

Repasemos sobre la figura el funcionamiento del modelo. Recordemos que a la derecha del
entorno aparecen las sentencias que se ejecutan:
1. En primer lugar se define la funcin makeContador() en el entorno global.
2. Despus se llama a esta funcin. Se crea un entorno local dentro del entorno global
en el que se ejecuta la funcin. Lo llamamos con el mismo nombre de la funcin,
makeContador. En l se crea la variable x con el valor 0 y se crea una funcin
annima sin parmetros que incremente el valor de x y lo devuelve. Esta funcin es

la que se devuelve como resultado de la ejecucin de makeContador() y se asigna a


la variable f (en el entorno global). La funcin queda asociada al entorno local en el
que se ha creado.
3. En el siguiente paso se invoca esta funcin. Se crea un entorno local dentro del
entorno de makeContador en el que se ejecuta la funcin. Se incrementa el valor de
x y se devuelve. La variable x a la que se accede es la variable local creada en el
entorno local makeContador.
4. Se vuelve a invocar f y vuelve a suceder lo mismo que en el caso anterior.
5. Por ltimo se intenta acceder al valor de x desde el entorno global. Se obtiene un
error, porque no existe ninguna variable x definida en este entorno.

Resumen del modelo de entornos


Despus de haber comprobado el funcionamiento de los entornos con los distintos ejemplos
que hemos presentado, podemos resumir el funcionamiento del modelo con las siguientes
reglas:
1. Las variables se definen en entornos, asociando un valor a su nombre.
2. Por defecto existe un entorno global.
3. Las instrucciones se ejecutan en los entornos. Cuando se referencia una variable en
un entorno se devuelve su valor definido en ese entorno. Si la variable no existe en
ese entorno, se busca en su entorno padre, hasta que se llega al entorno global.
4. Una invocacin a una funcin crea un mbito local, dentro del mbito en el que se
cre la funcin.
5. El cuerpo de la funcin se ejecuta en el mbito local creado por su invocacin.

Ms sobre el estado local


El concepto de estado local es muy importante. En programacin orientada a objetos el
estado se asocia a los objetos, pero hemos comprobado que es posible otro enfoque. La
mezcla de programacin funcional y programacin imperativa permite construir closures
(funciones) que almacenan estado local.
En el ejemplo anterior, la variable x es un estado local a la funcin que devuelve
makeContador. Hay que hacer notar que se crean tantos mbitos locales como invocaciones
a makeContador. Por ejemplo, podemos crear dos funciones, cada una con su propio estado
local:
var c1 = makeContador()
var c2 = makeContador()
c1() -> 1
c1() -> 2

c1() -> 3
c2() -> 1

Podemos modificar ligeramente la funcin anterior, aadindole un parmetro con el valor


inicial del contador:
def makeContador(i: Int) = {
var x = i
() => {
x = x + 1
x
}
}
var c1 = makeContador(100)
var c2 = makeContador(10)
c1() -> 101
c2() -> 11

Por ltimo, en el siguiente ejemplo podemos comprobar que es posible acceder al estado
local desde otros entornos (el global, en el caso del ejemplo):
import scala.collection.mutable.ListBuffer
def makeClosure() = {
var b = new ListBuffer[String]
(s: String) => {
b += s
b
}
}
var g = makeClosure()
var buf = g("Londres")
buf += "Roma"
g("Pars")

La funcin makeClosure() define un ListBuffer que se guarda en la variable local b y


crea una funcin annima de tipo (String) => ListBuffer[String]. La funcin recibe
una cadena y la aade a la variable local b. Despus se devuelve el listBuffer. Este
listBuffer ser un objeto compartido entre el entorno global y la closure que devuelve
makeClosure.
Una vez creada la funcin makeClosure(), se llama y se guarda su resultado (una closure
que aade cadenas a b) en la variable g. Cuando invocamos a g con una cadena, esta cadena
se aade a b y (lo ms importante) se devuelve el listBuffer modificado. Vemos que se
asigna a la variable buf. De esta forma hemos conseguido un dato mutable que puede ser
accedido desde el entorno global (con la variable buf) y desde la funcin g.
La siguiente figura muestra los entornos resultantes de la ejecucin del programa anterior.
Las variables buf y g se definen en el entorno global y la variable b en el entorno local
makeClosure. Los entornos locales g se producen por la invocacin (dos veces) a la
funcin g, una con el parmetro Londres y otra con el parmetro Pars. Los parmetros se
guardan en estos entornos en los que se ejecutan las sentencias de g.

Las variables buf y g son variables definidas en el entorno global, pero ambas referencian
objetos creados en el entorno local. La primera un listBuffer y la segunda una closure.
Las dos invocaciones a g se realizan desde el entorno global (con los parmetros Londres y
Pars) pero se ejecutan dentro del entorno local makeClosure, el entorno en el que se cre
la closure, creando los dos entornos locales llamados g que aparecen en la figura.
Veamos un ltimo ejemplo. Supongamos que queremos aadir al ejemplo del constructor
de contadores una variable local, pero compartida por todos los contadores. Queremos que

en esta variable local se acumulen los incrementos de todos los contadores creados. Cmo
lo podramos hacer?
La solucin pasa por utilizar en makeContador una variable llamada total que est
definida en un entorno superior:
def makeContador() = {
var x=0
() => {
total = total+1
x = x+1
(x,total)
}
}

Ahora makeContador, adems de declarar la variable local x que guarda el valor de cada
contador, utiliza una nueva variable total que va a estar compartida por todos los
contadores creados. Dnde declaramos total?. Podramos hacerlo en el entorno global,
pero esto permitira que otro programa externo a los contadores lo modificara. Queremos
que total se accesible slo desde los contadores.
La forma de hacerlo es creando makeContador dentro de otro entorno y para ello
necesitamos una nueva funcin de orden superior, que devuelva makeContador cuando la
invoquemos. Ser una funcin de segundo orden, que devuelve una funcin que, a su vez,
devuelve otra funcin. Llamamos a esta funcin: constructor:
def constructor() = {
var total=0
def makeContador() = {
var x=0
() => {
total = total+1
x = x+1
x
}
}
def getTotal() = {
total
}
(makeContador _, getTotal _)
}

Vemos que la funcin constructor() define la variable local total y despus las
funciones makeContador y getTotal. Por ltimo devuelve una pareja con ambas funciones
definidas. La variable total es una variable local accesible slo desde makeContador() y
getTotal().
El funcionamiento sera el siguiente:
scala> var funcs = constructor()
funcs: (() => () => Int, () => Int) = (<function0>,<function0>)
scala> var makeContador = funcs._1
makeContador: () => () => Int = <function0>

scala> var getTotal = funcs._2


getTotal: () => Int = <function0>
scala> var c1 = makeContador()
c1: () => Int = <function0>
scala> var c2 = makeContador()
c2: () => Int = <function0>
scala> c1()
res34: Int = 1
scala> c1()
res35: Int = 2
scala> c1()
res36: Int = 3
scala> c2()
res37: Int = 1
scala> getTotal()
res38: Int = 4

S-ar putea să vă placă și