Cuando crees que ya sabes Ruby, de repente te encuentras con algo que no conocías. A mí me pasó hace unas semanas con el splat operator (lo que David A. Black llama “the unary unarray operator” o “el operador unario de desarraymiento” :p )
El splat operator, que se representa con el asterisco en ruby, lo había visto para pasar un número variable de argumentos en definiciones de métodos, por ejemplo en el find de ActiveRecord, pero nunca me había planteado qué es lo que hacía exactamente.
Pues bien, básicamente lo que hace el splat operator es o bien separar un array en diferentes objetos, o bien agrupar diferentes objetos en un array, dependiendo si lo usamos a la derecha o a la izquierda de una expresión ruby.
Por ejemplo, en ruby podemos utilizar una expresión tal que
variable1,variable2=['valor1','valor2','valor3']
En ese caso, la variable1 acaba valiendo ‘valor1’ y la variable dos acaba valiendo ‘valor2’. El valor ‘valor3’ se pierde.
Usando el splat operator, podemos escribir esta expresión
variable1,*variable2=['valor1','valor2','valor3']
En este caso, la variable2 acabará siendo un array que contiene dos elementos [‘valor2′,’valor3’]. Éste es precisamente el uso que se hace cuando se define un argumento de una función con el splat operator. Todos los objetos que se pasen en la invocación al método a partir de ese argumento, se recibirán directamente como un array.
Si intentamos hacer algo como
variable1,*variable2,variable3=['valor1','valor2','valor3']
Obtendremos un error. El asterisco indica que se va tomar todo lo que venga a partir de esa posición para componer un array. Por este motivo, no podemos tener una variable para asignarse después, ya que no quedaría nada en el lado derecho de la expresión para ser asignado. Por este motivo, cuando usamos un número variable de argumentos solamente podemos aplicarlo en el último argumento de nuestro método.
Creo recordar que en una entrevista a Matz que lei una vez, hablaba de que una de las mejoras en Ruby 2 sería la posibilidad de definir argumentos variables al principio o al final de la cadena de argumentos, así que puede que el comportamiento del splat operator cambie para entonces, si finalmente se implementa esa funcionalidad.
Hasta aquí hemos visto cómo se comporta el splat cuando se usa en el lado izquierdo de una expresión, es decir, cuando lo usamos para convertir diferentes objetos en un array. Pero el splat tiene aplicaciones muy interesantes cuando se usa en el lado derecho de una expresión, es decir, cuando se usa para separar un array en diferentes objetos.
Uno de los casos interesantes es el uso en un when para ver si un valor está incluído en un array. Copio aquí un ejemplo directamente del antiguo blog de why
BOARD_MEMBERS = ['Jan', 'Julie', 'Archie', 'Stewick']
HISTORIANS = ['Braith', 'Dewey', 'Eduardo']
case name
when *BOARD_MEMBERS
"You're on the board! A congratulations is in order."
when *HISTORIANS
"You are busy chronicling every deft play."
end
Francamente interesante. En lugar de hacer un include?, directamente el splat expande el array in situ a sus valores, de forma que a todos los efectos es como si estuviera escribiendo inline el contenido del array separado por comas. Rizando el rizo, esto me permite hacer un case tal que
when 'admin'|*HISTORIANS|*BOARD_MEMBERS
Y por último, una de mis aplicaciones favoritas del splat operator: guardar en un array los argumentos para la llamada a una función que espera un número fijo de argumentos, y usar el splat para convertir el array en los argumentos individuales en el momento de la llamada.
Un ejemplo de la vida real. Tengo un método definido con la siguiente signatura
def Red.upload_indexes(producto_id, max_per_batch=REDCFG::MAX_PER_BATCH, output_file=nil )
Como se puede ver, este método puedo invocarlo con uno, dos o tres argumentos. Ahora quiero escribir un script que me permita lanzar este método desde la línea de comando mediante script/runner. Una opción sería modificar mi método para que reciba siempre un array y que en función de cuántos argumentos lleguen discrimine. Sería una solución, pero mi signatura pierde expresividad, y eso no me gusta.
¿Qué tal si hago lo siguiente?
ruby script/runner "Red.upload_indexes(*$ARGV[1..-1])" %1 %2 %3
¡¡¡Funciona!!! ¿Qué estamos haciendo exactamente? Estoy recogiendo el array de parámetros de entrada ARGV, y me quedo con todos los parámetros excepto el primero, que es el nombre del script. Ahora al array de argumentos le aplico el splat operator, con lo que se expande en sus variables individuales. Si me llaman con un argumento, se invocará a mi método con un sólo argumento, si se pasan dos o tres, se invocará correctamente.
Este mismo uso lo puedo aplicar con técnicas de metaprogramación, por ejemplo si estoy componiendo llamadas a métodos sobre la marcha y usando send.
mi_objeto.send('nombre_de_metodo',*array_con_los_argumentos)
Y ya por último, otro ejemplo más de la vida misma. En esta ocasión extendí la clase Array para que me de todos los elementos de un array excepto los que estén en las posiciones que a mí me interese. Es el inverso a values_at
class Array
def select_except(*indexes)
self.values_at(*(Array.new(self.size).fill{|i| i} - indexes))
end
end
Aquí me defino un método que acepta un número variable de índices. Después, me creo un nuevo array con tantas posiciones como el actual, y que tiene como contenido los índices en sí mismos (0,1,2…). Ahora tomo este array y le aplico la operación de resta entre Arrays, con lo que obtengo como resultado un nuevo Array en el que están todos los índices que quiero extraer de mi array.
Ahora ya sé todos los índices que necesito extraer, y tengo un método values_at que me permite pasar una lista de índices y me devuelve el array correspondiente. El problema es que values_at no me admite un Array como argumento. No hay problema, aplicando el splat operator, consigo separar el Array en sus objetos miembro, con lo que todo funciona. Para clarificarlo, las siguientes dos líneas de código son equivalentes
[obj0,obj1,obj2,obj3].values_at(0,3)
[obj0,obj1,obj2,obj3].values_at(*[0,3])
Y con esto, que no es poco, se acabó lo que el splat operator da de sí.
searchwords: splat operator, variable number of arguments, metaprogramming, unary unarray operator