|
|||
SongList, cuyas
especializaciones podrían ser catálogos y listas de reproducción.
SongList. Tenemos las siguientes opciones
obvias:
usar alguno de los tipos de Ruby Hash o
Array o, por contra, crear nuestra propia
estructura de
lista. Por pereza, de momento, vamos a echar un vistazo a arrays y
hashes y escogeremos una de ellas.
Array contiene una colección de
referencias a
objetos. Cada referencia ocupa una posición en el array, identificado
por un
índice entero no negativo.
Puedes crear arrays implícitamente utilizando literales o
explícitamente
creando un objeto Array. Un array literal es,
simplemente, una lista de objetos escritos entre corchetes.
a = [ 3.14159, "pie", 99 ]
|
||
a.type
|
» |
Array
|
a.length
|
» |
3
|
a[0]
|
» |
3.14159
|
a[1]
|
» |
"pie"
|
a[2]
|
» |
99
|
a[3]
|
» |
nil
|
|
||
b = Array.new
|
||
b.type
|
» |
Array
|
b.length
|
» |
0
|
b[0] = "second"
|
||
b[1] = "array"
|
||
b
|
» |
["second", "array"]
|
[]. Como la
mayoría de
operadores de Ruby, se trata de un método (de la clase
Array) y, por lo tanto, puede ser redefinido en
sus
subclases. Como muestra el ejemplo, los índices de los arrays comienzan
en
cero. Indexar un array con un único entero devuelve el objeto situado
en esa
posición o nil si dicha posición está vacía. Indexar un array
con un
entero negativo hace que se cuente a partir del final. Esto se ilustra
en la
Figura 4.1 en la página 35.
| Figura no disponible... |
a = [ 1, 3, 5, 7, 9 ]
|
||
a[-1]
|
» |
9
|
a[-2]
|
» |
7
|
a[-99]
|
» |
nil
|
[comienzo, cantidad]. Esto devolverá un nuevo array
compuesto
por referencias a cantidad objetos empezando en la posición
comienzo.
a = [ 1, 3, 5, 7, 9 ]
|
||
a[1, 3]
|
» |
[3, 5, 7]
|
a[3, 1]
|
» |
[7]
|
a[-3, 2]
|
» |
[5, 7]
|
a = [ 1, 3, 5, 7, 9 ]
|
||
a[1..3]
|
» |
[3, 5, 7]
|
a[1...3]
|
» |
[3, 5]
|
a[3..3]
|
» |
[7]
|
a[-3..-1]
|
» |
[5, 7, 9]
|
[] tiene relación directa con el operador
[]=,
que permite poner elementos en el array. Si se utiliza un sólo entero
como
índice, el elemento en la posición indicada es reemplazado por lo que
se
encuentre en la derecha de la asignación. Cualquier hueco que se
produzca será
rellenado con nil.
| a = [ 1, 3, 5, 7, 9 ] | » | [1, 3, 5, 7, 9] |
| a[1] = 'bat' | » | [1, "bat", 5, 7, 9] |
| a[-3] = 'cat' | » | [1, "bat", "cat", 7, 9] |
| a[3] = [ 9, 8 ] | » | [1, "bat", "cat", [9, 8], 9] |
| a[6] = 99 | » | [1, "bat", "cat", [9, 8], 9, nil, 99] |
[]= está formado por dos números (comienzo y
longitud) o un intervalo, se reemplazan esos elementos del array
original por
lo que se encuentre a la derecha de la asignación. Si la longitud es
cero, se
insertará esa parte derecha antes de la posición de comienzo, sin
eliminar
ningún elemento. Si el lado derecho es un array en sí mismo, se
utilizan sus
elementos en el reemplazo. El tamaño del array se ajusta
automáticamente si el
índice implica número diferente de elementos a los que hay en la parte
derecha
de la asignación.
| a = [ 1, 3, 5, 7, 9 ] | » | [1, 3, 5, 7, 9] |
| a[2, 2] = 'cat' | » | [1, 3, "cat", 9] |
| a[2, 0] = 'dog' | » | [1, 3, "dog", "cat", 9] |
| a[1, 1] = [ 9, 8, 7 ] | » | [1, 9, 8, 7, "dog", "cat", 9] |
| a[0..3] = [] | » | ["dog", "cat", 9] |
| a[5] = 99 | » | ["dog", "cat", 9, nil, nil, 99] |
=> valor
delimitados por llaves.
h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
|
||
|
||
h.length
|
» |
3
|
h['dog']
|
» |
"canine"
|
h['cow'] = 'bovine'
|
||
h[12] = 'dodecine'
|
||
h['cat'] = 99
|
||
h
|
» |
{"cow"=>"bovine", "cat"=>99, 12=>"dodecine", "donkey"=>"asinine", "dog"=>"canine"}
|
SongList. Vamos a crear
una lista básica de métodos que pueden resultar necesarios en nuestra
SongList.
Querremos añadir más según vayamos avanzando, pero de momento bastará
con
estos.
Array. Del mismo modo, esta clase ofrecerá la
posibilidad
de devoler una canción en base a un índice entero.
Sin embargo, también debería ser posible acceder a las canciones por su
título, lo que sugeriría el uso de un hash con el título como clave y
la
canción como valor. ¿Podríamos entonces utilizar un hash? Posiblemente,
pero
hay algunos problemas. Para empezar, un hash no tiene orden, así que
seguramente deberíamos usar un array auxiliar para mantener el orden de
la
lista de canciones. Y aún existe un problema mayor: los hashes no son
capaces
de asociar múltiples claves a un mismo valor. Esto podría ser un
inconveniente
para una lista de reproducción, ya que existe la posibilidad de que se
repita
alguna canción para que suene más de una vez. Así que, por ahora, nos
apañaremos con un array de canciones, buscando entre los títulos cuando
lo
necesitemos. Si este se convirtiera en un problema para el rendimiento,
siempre
podríamos añadir algún tipo de búsqueda basada en un hash más adelante.
Comenzaremos nuestra clase con el método básico
initialize, que
crea el Array que usaremos para guardar las
canciones y
almacenar las referencias a éstas en la variable de instancia
@songs.
class SongList def initialize @songs = Array.new end end |
SongList#append añade
una
canción al final del array @songs. Además, devuelve
self, una referencia al propio objeto
SongList. Esta es una convención muy útil, ya
que
permite encadenar múltiples llamadas a append, como
veremos
posteriormente en un ejemplo.
class SongList def append(aSong) @songs.push(aSong) self end end |
deleteFirst y
deleteLast, implementados de forma trivial utilizando
Array#shift
y
Array#pop
,
respectivamente.
class SongList def deleteFirst @songs.shift end def deleteLast @songs.pop end end |
append devuelva el objeto
SongList para encadenar las llamadas a este
método,
como se mencionó anteriormente.
list = SongList.new
list.
append(Song.new('title1', 'artist1', 1)).
append(Song.new('title2', 'artist2', 2)).
append(Song.new('title3', 'artist3', 3)).
append(Song.new('title4', 'artist4', 4))
|
nil cuando
se
vacía la lista.
list.deleteFirst
|
» |
Song: title1--artist1 (1)
|
list.deleteFirst
|
» |
Song: title2--artist2 (2)
|
list.deleteLast
|
» |
Song: title4--artist4 (4)
|
list.deleteLast
|
» |
Song: title3--artist3 (3)
|
list.deleteLast
|
» |
nil
|
[], que
accede a los
elementos por medio de un índice. Si se trata de un número (lo que
comprobaremos utilizando
Object#kind_of?
),
simplemente devolveremos el elemento que se encuentre en esa posición.
class SongList def [](key) if key.kind_of?(Integer) @songs[key] else # ... end end end |
list[0]
|
» |
Song: title1--artist1 (1)
|
list[2]
|
» |
Song: title3--artist3 (3)
|
list[9]
|
» |
nil
|
SongList es
implementar el método [], que toma una cadena y busca una
canción
con ese título. Esto parece algo bastante directo: tenemos un array de
canciones, así que simplemente recorremos todos los elementos buscando
la
coincidencia en el título.
class SongList def [](key) if key.kind_of?(Integer) return @songs[key] else for i in 0...@songs.length return @songs[i] if key == @songs[i].name end end return nil end end |
for iterando sobre
un
array. ¿Qué podría ser más natural?
Pues he aquí que existe algo más natural aún. En cierto modo, nuestro
bucle
for ``intima'' con el array; le pregunta por su longitud y
entonces va
recuperando valores hasta que encuentra lo que busca. ¿Por qué no,
simplemente, pedirle al array que haga esta comparación a todos sus
miembros?
Esto es, ni más ni menos, lo que hace el método find de la
clase
Array.
class SongList
def [](key)
if key.kind_of?(Integer)
result = @songs[key]
else
result = @songs.find { |aSong| key == aSong.name }
end
return result
end
end
|
if.
class SongList
def [](key)
return @songs[key] if key.kind_of?(Integer)
return @songs.find { |aSong| aSong.name == key }
end
end
|
find es un iterador---un método que invoca un
bloque de
código repetidamente. Los iteradores y los bloques de código están
entre las
funcionalidades más características de Ruby, así que vamos a detenernos
profundizar en ellos (y, de paso, explicaremos qué es lo que hace
exactamente
esa línea de código de nuestro método []).
yield. Cuando se ejecuta un
yield, se invoca al código contenido en el bloque. Cuando se
sale del
bloque, la ejecución continúa por la sentencia que sucede al
yield.
[A los entusiastas de los lenguajes de programación les
gustará saber
que la palabra reservada yield fue escogida imitando a la
función
yield del lenguaje CLU de Liskov's. Dicho lenguaje tiene
unos 20
años de antiguedad y muchas de sus funcionalidades no se encuentran
aún muy
extendidas.] Empecemos con un ejemplo sencillo.
def threeTimes
yield
yield
yield
end
threeTimes { puts "Hello" }
|
Hello Hello Hello |
threeTimes. Dentro de este método, se invoca a
yield
tres veces por fila. Cada vez que se ejecuta el código contenido en el
bloque
se imprime un saludo. Sin embargo, lo que hace a los bloques más
interesantes
es la posibilidad de poder pasarles parámetros y que te devuelvan
valores. Por
ejemplo, podríamos escribir función sencilla que devolviera los
primeros
miembros de la serie de Fibonacci.[La serie básica de
Fibonacci
consiste en una secuencia de enteros que comienza con dos "unos", de
modo
que los siguientes términos se calculan a partir de la suma de los
dos
anteriores. En ocasiones, las series se utilizan en algoritmos de
ordenación
y análisis de fenómenos naturales.]
def fibUpTo(max)
i1, i2 = 1, 1 # parallel assignment
while i1 <= max
yield i1
i1, i2 = i2, i1+i2
end
end
fibUpTo(1000) { |f| print f, " " }
|
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 |
yield tiene un parámetro. Este
valor se
pasa al bloque asociado. En la definición del bloque, la lista de
argumentos
aparece entre barras verticales. En esta caso, la variable
f recibe el valor que se le pasó a yield, así que
el
bloque imprime los sucesivos miembros de la serie (Puede verse incluso
un
ejemplo de asignación paralela en este fragmento de código. Volveremos
sobre
ese tema en la página 75). A pesar de que es normal pasar sólo un valor
al
bloque, no es obligatorio: puede tomar cualquier número de
argumentos. Pero, ¿qué ocurre si el bloque espera un número de
parámetros distinto
del que se le pasa al yield? Por asombrosa casualidad, aquí
entran
en juego las reglas de asignación paralela (con un ligero cambio: los
múltiples parámetros que se pasan a yield se convierten en un
array
si el bloque tiene tan sólo un argumento).
Los parámetros que se pasan a un bloque pueden ser variables locales ya
existentes; si es así, los nuevos valores de las variables serán
conservados
cuando la ejecución de bloque haya terminado. Es posible que este sea
un
comportamiento inesperado, pero se puede obtener cierta ganancia de
rendimiento al reutilizar variables que ya existen.[Para más
información acerca de éste y otros FIXME ``gotchas'', ver la lista
que
comienza en la página 127; más información acerca de rendimiento en
la
128.]
Un bloque puede, incluso, devolver un valor al método. El valor de la
última
expresión evaluada en el bloque se entrega al método como el valor del
yield. Así es como el método find, utilizado por
la
clase Array, funciona.[El método
find se
encuentra definido en el módulo Enumerable,
que está
"combinado" (mix-in) con la clase
Array.] Su
implementación podría ser algo parecido a lo siguiente:
class Array
|
||
def find
|
||
for i in 0...size
|
||
value = self[i]
|
||
return value if yield(value)
|
||
end
|
||
return nil
|
||
end
|
||
end
|
||
|
||
[1, 3, 5, 7, 9].find {|v| v*v > 30 }
|
» |
7
|
true, el método retorna el elemento
correspondiente.
Si no coincide ningún elemento, el método devuelve nil. El
ejemplo
ilustra el beneficio de esta aproximación mediante iteradores. La clase
Array hace lo que mejor sabe: acceder a los
elementos,
dejando al código de la aplicación concentrarse en sus propias
necesidades (en
este caso, encontrar una entrada que cumpla con algún criterio
matemático).
Algunos iteradores son comunes a muchos tipos de colecciones de Ruby.
find ya lo hemos visto con anterioridad. Los otros dos
métodos
comunes son each y collect. each
es,
probablemente, el iterador más simple---se limita a hacer un
yield de los elementos de una colección.
[ 1, 3, 5 ].each { |i| puts i }
|
1 3 5 |
each tiene un lugar especial en Ruby; en la
página 85
describiremos su uso como base del bucle for, y a partir de la
102
veremos cómo, definiendo un método each, puedes añadir
funcionalidad a tus clases de forma sencilla.
El otro iterador común es collect, que toma un elemento de
la
colección y se lo pasa al bloque. Los resultados que el bloque devuelve
se
usan para construir un nuevo array. Por ejemplo:
["H", "A", "L"].collect { |x| x.succ }
|
» |
["I", "B", "M"]
|
yield
cada
vez que genera un nuevo valor. FIXME The thing that uses the iterator
is simply
a block of code associated with this method. No hay necesidad de crear
clases
de apoyo para llevar el estado del iterador, como en Java y C++. En
este y
otros aspectos, Ruby es un lenguaje transparente. Cuando escribes un
programa
en Ruby, te concentras en que haga lo que tiene que hacer, y no en
desarrollar
una base en la que apoyar el propio lenguaje.
Los iteradores no sólo pueden acceder a los datos contenidos en arrays
y
hashes. Como se vio en el ejemplo de la serie de Fibonacci, un iterador
puede
devolver valores derivados. La entrada/salida de Ruby hace uso de esta
capacidad, implementando un iterador interfaz que devuelve sucesivas
líneas (o
bytes) en un flujo de E/S.
f = File.open("testfile")
f.each do |line|
print line
end
f.close
|
This is line one This is line two This is line three And so on... |
inject.
sumOfValues "Smalltalk method" ^self values inject: 0 into: [ :sum :element | sum + element value] |
inject funciona de este modo: la primera vez que
se
llama al bloque asociado, sum se inicializa al valor del
parámetro
de inject (cero en este caso) y element al del
primer
elemento del array. La segunda y sucesivas llamadas, sum
toma el
valor que devolvió el bloque en la llamada anterior. De este modo,
sum puede utilizarse como acumulador. El valor final de
inject es el devuelto por el bloque la última vez que se
le
llamó.
Ruby no tiene un método inject, pero es fácil escribirlo.
Ahora
vamos a añadírselo a la clase Array, pero más
adelante,
en la página 100, veremos cómo hacerlo mucho más general.
class Array
|
||
def inject(n)
|
||
each { |value| n = yield(n, value) }
|
||
n
|
||
end
|
||
def sum
|
||
inject(0) { |n, value| n + value }
|
||
end
|
||
def product
|
||
inject(1) { |n, value| n * value }
|
||
end
|
||
end
|
||
[ 1, 2, 3, 4, 5 ].sum
|
» |
15
|
[ 1, 2, 3, 4, 5 ].product
|
» |
120
|
class File
def File.openAndProcess(*args)
f = File.open(*args)
yield f
f.close()
end
end
File.openAndProcess("testfile", "r") do |aFile|
print while aFile.gets
end
|
This is line one This is line two This is line three And so on... |
openAndProcess es un método de clase---puede ser
invocado independientemente de cualquier objeto
File.
Le hemos puesto los mismos argumentos que el método convencional
File.open
,
pero realmente no nos preocupamos acerca de qué parámetros se trata. En
su
lugar, hemos especificado los argumentos como *args, lo que
significa
``recoge los parámetros pasados al método y mételos en un array''.
Entonces,
llamamos a File.open pasándole *args como parámetro.
Esto
expande el array dando lugar a los parámetros individuales. El
resultado es
que openAndProcess pasa, de forma transparente, los parámetros
que
recibe a
File.open
.
Una vez que el fichero ha sido abierto, openAndProcess llama
yield, pasando el objeto del fichero abierto al bloque.
Cuando se
vuelve del bloque, el fichero se cierra. De este modo, la
responsabilidad de
cerrar un fichero abierto ha sido trasladada del usuario de los objetos
de
ficheros a los ficheros en sí mismos.
Finalmente, este ejemplo usa do...end para definir un
bloque. La única diferencia entre esta notación y las llaves es la
precedencia: do...end tiene menor peso que ``{...}''.
Discutiremos su impacto en la página 234.
Tener ficheros que manejen su ciclo de vida por sí mismo es tan útil
que la clase
File que viene con Ruby implementan esta
característica. Si
File.open
tiene un bloque asociado, éste será invocado con un objeto fichero que
se
cerrará al terminar la ejecución del bloque. Este resulta interesante
ya que
File.open
tiene dos comportamientos distintos: si se lo llama con un bloque, lo
ejecuta
y cierra el fichero. Si no hay bloque alguno, devuelve el objeto
fichero. Esto es posible gracias al método
Kernel::block_given?
,
que devuelve true si el método actual tiene un bloque
asociado.
Haciendo uso de este método, podrías implementar
File.open
(de nuevo ignorando el manejo de errores) de la forma siguiente.
class File def File.myOpen(*args) aFile = File.new(*args) # If there's a block, pass in the file and close # the file when it returns if block_given? yield aFile aFile.close aFile = nil end return aFile end end |
bStart = Button.new("Start")
bPause = Button.new("Pause")
# ...
|
Button, el hardware invocará a una función
"folks rigged" FIXME.
La forma más obvia de añadir funcionalidad a estos botones es crear
subclases
de Button que implementen sus propios métodos
buttonPressed.
What happens when the user presses one of our buttons? In the
Button class, the hardware folks rigged things
so that a
callback method, buttonPressed, will be invoked.
The obvious way of adding functionality to these buttons is to create
subclasses of Button and have each subclass
implement its own
buttonPressed method.
class StartButton < Button
def initialize
super("Start") # invoke Button's initialize
end
def buttonPressed
# do start actions...
end
end
bStart = StartButton.new
|
Button
podría implicar un montón de trabajo de mantenimiento. El segundo
problema es
que las acciones realizadas cuando se pulsa un botón están expresadas
al nivel equivocado;
realmente no se trata de funcionalidades de los botones, sino de la
máquina de
discos que hace uso de ellos. Ambos problemas pueden arreglarse
haciendo
empleando bloques.
class JukeboxButton < Button
def initialize(label, &action)
super(label)
@action = action
end
def buttonPressed
@action.call(self)
end
end
bStart = JukeboxButton.new("Start") { songList.start }
bPause = JukeboxButton.new("Pause") { songList.pause }
|
JukeboxButton#initialize. Si el
último
argumento en la definición de un método tiene como prefijo un
``ampersand''
(como &action), Ruby busca un bloque de código en el lugar
en el
que se haga la llamada. Este bloque se convierte a un objeto de la
clase
Proc, se asigna al parámetro precedido del
``ampersand'' y puede tratarse como cualquier otra variable. En nuestro
ejemplo, lo hemos asignado a @action. Al llamar a
buttonPressed, se usa el método
Proc#call
para invocar al bloque.
Entonces, ¿qué tenemos exactamente cuando creamos un objeto
Proc? Lo interesante es que es más que un trozo
de
código. Todo el contexto en el que se define el bloque está
asociado
con él: el valor de self y los métodos, variables y
constantes
del ámbito. Y ahí está parte de la magia de Ruby: el bloque puede
seguir
usando la información de su ámbito original incluso si el entorno en el
que ha
sido definido ha desaparecido. En otros lenguajes, esta característica
se
conoce como closure.
Echemos un vistazo a un ejemplo artificial, donde se usa el método
proc que convierte el bloque en un objeto
Proc.
def nTimes(aThing)
|
||
return proc { |n| aThing * n }
|
||
end
|
||
|
||
p1 = nTimes(23)
|
||
p1.call(3)
|
» |
69
|
p1.call(4)
|
» |
92
|
p2 = nTimes("Hello ")
|
||
p2.call(3)
|
» |
"Hello Hello Hello "
|
nTimes devuelve un objeto
Proc
que hace referencia al parámetro aThing. Incluso en el caso de
que el
parámetro se encuentre fuera del ámbito en el momento en que se llama
al
bloque, permanecerá accesible para el bloque.