Documente Academic
Documente Profesional
Documente Cultură
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)
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.
Datos mutables
Mutadores en Scheme
(set! <simbolo> <expresion>)
(cons 1 2))
(cons 1 2))
a)
b)
c)
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))))
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
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))
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() = {
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
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.
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
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()
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
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
c1() -> 3
c2() -> 1
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")
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>