¿Cómo funciona... ? Argumentos de métodos - Parte 2 (Ruby 3 y otras cosas)
Este post es continuación de este otro: https://analizandoruby.blogspot.com/2020/01/como-funciona-argumentos-en-metodos.html (recomiendo leerlo antes de este si no lo hicieron!)
Hoy vamos a entrar un poco más en detalle en algunos cambios que introdujo Ruby 2.7 y además unos deprecation warning para preparar el código para migrar, en un futuro, a Ruby 3. También vamos a ver algunas cosas que me quedaron por comentar y nuevos features relacionados a los argumentos.
¿Qué va a cambiar en Ruby 3?
Los cambios son fáciles de hacer y están bien detallados en el link oficial (al final en la sección de Recursos al final) por lo que no debería haber ningún problema mayor si hacemos las cosas con cuidado con Ruby 2.7.
En el posteo anterior vimos este ejemplo bajo el título "Combinando argumentos posicionales y con nombres":
Y comentamos cómo es equivalente llamar al método foo usando un hash:
Si lo ejecutamos en Ruby 2.7 vamos a ver ese warning que antes no estaba, ya no va a ser equivalente en Ruby 3!
En este caso puntual simplemente se podrían sacar las llaves para evitar el problema. Veamos este otro ejemplo donde los argumentos son una variable definida previamente:
Ruby 2.x automáticamente convierte el hash en keyword arguments, pero esto podría traer problemas si queremos efectivamente pasar un hash.
La solución para este caso es la indicada en el warning, usar el operador "double splat" (o **):
Hay algunos casos más así de específicos que están los ejemplos en el link oficial, son similares en cuanto a que dependen de combinar argumentos posicionales con keyword arguments, el orden y el tipo de dato si es hash o no. Lo importante es que en Ruby 3 vamos a tener que ser explícitos cuando pasemos argumentos si queremos un hash o keyword arguments como último argumento al llamar una función, personalmente prefiero el comportamiento explícito.
¿Qué son los operadores * y **?
En el primer post me faltó comentar estos dos operadores a pesar de haberlos usado. Lo que hacen es separar un array o un hash en una lista de argumentos. Para arrays se usa * y para hashes, **. Un ejemplo es mucho más claro
Podemos ver claramente la diferencia: en el primer caso, el array args se pasa como primer argumento a la función; en el segundo caso, el array args se divide en dos argumentos diferentes para la función.
Esto suele ser útil cuando los argumentos vienen de otro lado o los vamos armando antes guardando en un array lo que necesitamos según distintas acciones.
Ocurre exactamente lo mismo con los hashes, excepto que al usarlos como último argumento Ruby 2.x automáticamente los separa como keyword arguments aunque no lo indiquemos usando el operador **. El funcionamiento de Ruby 3 es más consistente al no hacer algo distinto con un caso en particular.
¿Cómo delegamos argumentos a super?
Es común, al programar orientado a objetos, aplicar herencia y sobreescritura de métodos. Normalmente se sobreescribe un método de una clase padre para agregar algún comportamiento nuevo y además se invoca el método original usando la palabra clave "super" (en el caso de Ruby) para mantener el comportamiento anterior en caso de que corresponda. Veamos en código:
Como vemos, al llamar a super no necesitamos indicarle ningún argumento, Ruby pasa como argumento las mismas variables. Ojo con eso porque si antes de llamara a super modificamos alguno de los argumentos, el método padre recibe el dato cambiado y no el original (*):
(*) Esto no pasa en https://ruby.github.io/TryRuby/, pero sí usando IRB
También podemos ser explícitos e incluso llamar a super con otros argumentos si queremos (aunque ya dejaría de ser una delegación de argumentos, sería un llamado distinto):
¿Cómo delegamos argumentos a otros métodos?
A veces nuestro método no sobreescribe otro método pero si respeta los mismos argumentos, por lo que no podemos llamar a super entonces si o si tenemos que llamar a ese método y pasar todos los argumentos:
También deberíamos hacer lo mismo para todos los tipos de argumentos que definamos y ya vimos en ejemplos anteriores que la firma del método puede ser muy larga. Para ahorrar un poco de código, Ruby 2.7 introduce la nueva sintaxis "..." para delegar todos los argumentos:
La única consideración en este caso es que no tenemos acceso fácil a los argumentos para usarlos (seguramente con algo de meta programación se pueda, pero en ese caso es mejor no usar este nuevo operador y declarar los argumentos de forma explícita).
Concusión: ¿Tengo que cambiar mucho código?
En principio, antes de pasar a Ruby 3 (cuando salga) es necesario pasar si o si por Ruby 2.7 para poder ver todos los warnings, porque luego va a ser tarde. Los cambios son chicos y la mayoría son situaciones raras, pero es difícil saber qué hacen internamente las gemas que usamos por lo que hay que hacerlo con cuidado y tener una buena suite de tests para poder detectar la mayor cantidad de warnings posible. También se puede usar método Module#ruby2_keywords para mantener el funcionamiento previo (o usar esta gema si queremos implementar el cambio para que funcione incluso en Ruby 2.6 o anterior https://rubygems.org/gems/ruby2_keywords) pero yo no lo recomendaría salvo que sea necesario, en lo posible es mejor pasar a definir argumento de forma explícita si queremos que sea un hash o keywords.
Hay algunos detalles menores en el link oficial con pequeñas diferencias entre Ruby 2.6 y 2.7 en cuando al parseo de los argumentos, son casos raros y no me parece interesante detallarlos cuando en ese link están claros y no se usan tanto. Justo estamos en un momento en la vida de Ruby en que tenemos que tener especial cuidado con los argumentos.
Recursos:
- Post oficial explicando los cambios y los motivos: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
Hoy vamos a entrar un poco más en detalle en algunos cambios que introdujo Ruby 2.7 y además unos deprecation warning para preparar el código para migrar, en un futuro, a Ruby 3. También vamos a ver algunas cosas que me quedaron por comentar y nuevos features relacionados a los argumentos.
¿Qué va a cambiar en Ruby 3?
Los cambios son fáciles de hacer y están bien detallados en el link oficial (al final en la sección de Recursos al final) por lo que no debería haber ningún problema mayor si hacemos las cosas con cuidado con Ruby 2.7.
En el posteo anterior vimos este ejemplo bajo el título "Combinando argumentos posicionales y con nombres":
def foo(arg1, arg2 = 'por defecto', arg3:, arg4: 'por defecto4', arg5:, arg6: 'por defecto6')
puts "arg1 es: #{arg1.inspect}"
puts "arg2 es: #{arg2.inspect}"
puts "arg3 es: #{arg3.inspect}"
puts "arg4 es: #{arg4.inspect}"
puts "arg5 es: #{arg5.inspect}"
puts "arg6 es: #{arg6.inspect}"
end
> foo(1, arg3: 3, arg5: 5)
arg1 es: 1
arg2 es: "por defecto"
arg3 es: 3
arg4 es: "por defecto4"
arg5 es: 5
arg6 es: "por defecto6"
Y comentamos cómo es equivalente llamar al método foo usando un hash:
> foo(1, {arg3: 3, arg5: 5})
# warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the callarg1 es: 1
arg2 es: "por defecto"
arg3 es: 3
arg4 es: "por defecto4"
arg5 es: 5
arg6 es: "por defecto6"
Si lo ejecutamos en Ruby 2.7 vamos a ver ese warning que antes no estaba, ya no va a ser equivalente en Ruby 3!
En este caso puntual simplemente se podrían sacar las llaves para evitar el problema. Veamos este otro ejemplo donde los argumentos son una variable definida previamente:
> h = {arg3: 3, arg5: 5}
> foo(1, h)
# warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call>arg1 es: 1
arg2 es: "por defecto"
arg3 es: 3
arg4 es: "por defecto4"
arg5 es: 5
arg6 es: "por defecto6"
Ruby 2.x automáticamente convierte el hash en keyword arguments, pero esto podría traer problemas si queremos efectivamente pasar un hash.
La solución para este caso es la indicada en el warning, usar el operador "double splat" (o **):
> h = {arg3: 3, arg5: 5}
> foo(1, **h)
arg1 es: 1
arg2 es: "por defecto"
arg3 es: 3
arg4 es: "por defecto4"
arg5 es: 5
arg6 es: "por defecto6"
Hay algunos casos más así de específicos que están los ejemplos en el link oficial, son similares en cuanto a que dependen de combinar argumentos posicionales con keyword arguments, el orden y el tipo de dato si es hash o no. Lo importante es que en Ruby 3 vamos a tener que ser explícitos cuando pasemos argumentos si queremos un hash o keyword arguments como último argumento al llamar una función, personalmente prefiero el comportamiento explícito.
¿Qué son los operadores * y **?
En el primer post me faltó comentar estos dos operadores a pesar de haberlos usado. Lo que hacen es separar un array o un hash en una lista de argumentos. Para arrays se usa * y para hashes, **. Un ejemplo es mucho más claro
def foo(arg1, arg2 = 'defecto2')
puts "arg1 es: #{arg1}"
puts "arg2 es: #{arg2}"
end
> args = [1, 2]
> foo(args)
arg1 es: [1, 2] # arg1 es el array
arg2 es: defecto2
> foo(*args)
arg1 es: 1 # arg1 es el elemento 1
arg2 es: 2 # arg2 es el elemento 2
Podemos ver claramente la diferencia: en el primer caso, el array args se pasa como primer argumento a la función; en el segundo caso, el array args se divide en dos argumentos diferentes para la función.
Esto suele ser útil cuando los argumentos vienen de otro lado o los vamos armando antes guardando en un array lo que necesitamos según distintas acciones.
Ocurre exactamente lo mismo con los hashes, excepto que al usarlos como último argumento Ruby 2.x automáticamente los separa como keyword arguments aunque no lo indiquemos usando el operador **. El funcionamiento de Ruby 3 es más consistente al no hacer algo distinto con un caso en particular.
¿Cómo delegamos argumentos a super?
Es común, al programar orientado a objetos, aplicar herencia y sobreescritura de métodos. Normalmente se sobreescribe un método de una clase padre para agregar algún comportamiento nuevo y además se invoca el método original usando la palabra clave "super" (en el caso de Ruby) para mantener el comportamiento anterior en caso de que corresponda. Veamos en código:
class Bar
def something(arg1, arg2)
puts arg1
puts arg2
end
end
class Foo < Bar
def something(arg1, arg2)
super # llamo al método original
puts arg1 + arg2 #luego ejecuto otro código que no estaba en el método original
end
end
> Bar.new.something(1, 2)
1
2
> Foo.new.something(1, 2)
1
2
3
Como vemos, al llamar a super no necesitamos indicarle ningún argumento, Ruby pasa como argumento las mismas variables. Ojo con eso porque si antes de llamara a super modificamos alguno de los argumentos, el método padre recibe el dato cambiado y no el original (*):
class Foo < Bar
def something(arg1, arg2)
arg1 = arg1 + 5
super
end
end
> Foo.new.something(1, 2)
6 # <<< no es 1, es 6
2
(*) Esto no pasa en https://ruby.github.io/TryRuby/, pero sí usando IRB
También podemos ser explícitos e incluso llamar a super con otros argumentos si queremos (aunque ya dejaría de ser una delegación de argumentos, sería un llamado distinto):
class Foo < Bar
def something(arg1, arg2)
super(arg1 * 2, arg2) # llamo al método original con otros valores
end
end
¿Cómo delegamos argumentos a otros métodos?
A veces nuestro método no sobreescribe otro método pero si respeta los mismos argumentos, por lo que no podemos llamar a super entonces si o si tenemos que llamar a ese método y pasar todos los argumentos:
class Foo < Bar
def another_thing(arg1, arg2)
something(arg1, arg2)
end
end
También deberíamos hacer lo mismo para todos los tipos de argumentos que definamos y ya vimos en ejemplos anteriores que la firma del método puede ser muy larga. Para ahorrar un poco de código, Ruby 2.7 introduce la nueva sintaxis "..." para delegar todos los argumentos:
class Foo < Bar
def another_thing(...)
# podría hacer algo no relacionado a los argumentos
something(...)
end
end
La única consideración en este caso es que no tenemos acceso fácil a los argumentos para usarlos (seguramente con algo de meta programación se pueda, pero en ese caso es mejor no usar este nuevo operador y declarar los argumentos de forma explícita).
Concusión: ¿Tengo que cambiar mucho código?
En principio, antes de pasar a Ruby 3 (cuando salga) es necesario pasar si o si por Ruby 2.7 para poder ver todos los warnings, porque luego va a ser tarde. Los cambios son chicos y la mayoría son situaciones raras, pero es difícil saber qué hacen internamente las gemas que usamos por lo que hay que hacerlo con cuidado y tener una buena suite de tests para poder detectar la mayor cantidad de warnings posible. También se puede usar método Module#ruby2_keywords para mantener el funcionamiento previo (o usar esta gema si queremos implementar el cambio para que funcione incluso en Ruby 2.6 o anterior https://rubygems.org/gems/ruby2_keywords) pero yo no lo recomendaría salvo que sea necesario, en lo posible es mejor pasar a definir argumento de forma explícita si queremos que sea un hash o keywords.
Hay algunos detalles menores en el link oficial con pequeñas diferencias entre Ruby 2.6 y 2.7 en cuando al parseo de los argumentos, son casos raros y no me parece interesante detallarlos cuando en ese link están claros y no se usan tanto. Justo estamos en un momento en la vida de Ruby en que tenemos que tener especial cuidado con los argumentos.
Recursos:
- Post oficial explicando los cambios y los motivos: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
Comentarios
Publicar un comentario