¿Cómo funciona... ? Argumentos en métodos
Link: https://docs.ruby-lang.org/en/2.7.0/syntax/methods_rdoc.html#label-Arguments
Hoy vamos a ver algo diferente, que en principio puede parecer básico pero Ruby tiene muchas opciones para pasar argumentos a los métodos que resultan muy interesantes. Vamos a ir definiendo unos métodos agregando complejidad
¿Qué es un método?
Un método es un concepto asociado a la programación orientada a objetos (OOP), es similar a lo que en otros lenguajes se conoce como "función" pero, como en Ruby TODO son objetos, no hay "funciones" sino sólo "métodos".
Los métodos y funciones agrupan código para poder ser ejecutado más fácilmente sin necesidad de repetir ese código en los lugares que se necesite correr. La diferencia principal, conceptualmente hablando, es que los métodos se ejecutan en el contexto de un objeto instancia de una clase pudiendo cambiar su resultado según el estado de dicho objeto; en cambio las funciones se ejecutan como código aislado cuyo resultado sólo depende de los datos de entrada.
En algunos lenguajes como Ruby no tenemos el concepto de "función" porque todo son objetos, entonces siempre los bloques de código agrupados son métodos. En otros lenguajes ocurre lo opuesto, en Elixir, por ejemplo, no existen los objetos, por lo que no existen los métodos, los bloques de código agrupado son siempre funciones. También hay lenguajes que soportan ambos conceptos: tanto funciones aisladas como objetos con métodos. En esos lenguajes, las funciones y los métodos se suelen definir de la misma forma y cambia su significado según el contexto (si defino una función dentro de la declaración de un objeto, esta es conceptualmente un método de ese objeto).
Si en Ruby definimos un método que en principio podría parecer aislado, este se define como un método del objeto main, por lo que no es una función sino un bloque de código que se ejecuta en el contexto de un objeto.
¿Qué son los argumentos?
Un método puede o no necesitar datos externos para ejecutar su código. Un método que no necesita datos externos podría ser por ejemplo este:
Recordemos que en Ruby no necesitamos la palabra clave "return", un método siempre devuelve el resultado de la última línea de código si no le indicamos otra cosa.
Un método que sí necesite datos podría leerlos directamente del objeto al que pertenece o recibir datos al ser invocado. Como no estamos tratando de enseñar conceptos de OOP vamos a hablar sólo de los argumentos que se pasan al invocar un método. Recién vimos el método "pi", este método tiene 0 argumentos (la cantidad de argumentos se llama "aridad").
Vamos a agregar entonces argumentos a una función y vamos a simplificarla para mostrar lo que hace cada argumento en lugar de mostrar un resultado fijo como el valor de Pi.
Argumentos requeridos
El primer ejemplo es simple, un método que tiene 1 (o más) argumento requeridos:
Este método recibe un dato e imprime el nombre de la variable que asignamos y su valor.
Para invocar el método foo si o si le tenemos que pasar un argumento, si no lo hacemos da error:
Podemos tener la cantidad de argumento que queramos:
Si bien se puede, está considerado mala práctica o un "code smell" tener demasiados argumentos porque el código se vuelve más complejo. Se puede, pero hay que hacerlo con criterio. En algunas guías de estilo dicen que 5 debería ser el máximo de argumentos de una función.
Argumentos opcionales
Si queremos que nuestro método acepte un argumento pero que no sea requerido, podemos asignarle un valor por defecto:
Entonces:
Combinando argumentos requeridos y opcionales
Ahora podemos agregar más argumentos y combinar requeridos y combinados de distintas formas:
En algunos lenguajes necesitamos que los argumentos opcionales estén al final, en Ruby no.
Acá se vuelve un poco confuso... Si la cantidad de argumentos que usamos al invocar al método es igual al total no pasa nada raro:
Pero si es menor que el total, primero se asignan los argumentos requeridos con el final de los que indicamos y luego los opcionales con los primeros que indicamos. Un ejemplo es más claro:
Es importante que los argumentos opcionales estén agrupados (no es necesario agrupar los requeridos):
Argumento opcional con valor por defecto dependiente de otro argumento
Podemos asignar como valor por defecto de un argumento, una transformación de otro argumento!
Incluso podemos usar el resultado de otro método usando el valor de otro argumento:
El único requerimiento para que esto funcione es que el argumento utilizado tiene que estar definido a la izquierda del que lo usa. Esto no es válido:
En este caso, "arg3" aún no está definido al llamar el método y Ruby intenta buscar un método "arg3".
Argumentos con nombre (keyword arguments)
A veces es dificil recordar el orden de los argumentos posicionales. Para evitar este problema se pueden definir argumentos con nombre (o keyword arguments) que, a diferencia de los posicionales, no tienen que ponerse en ningún orden en particular al invocar el método. Cambia un poco la sintaxis:
Los argumentos requeridos ahora terminan en ":" y los opcionales no llevan el "=". Ya no podemos invocar el método con foo(1) porque Ruby espera que los argumentos indiquen el nombre del argumento al que pertenecen en lugar de usar su posición. Entonces tenemos que hacer:
Combinando argumentos posicionales y con nombres
En este ejemplo tenemos muchos conceptos mezclados: tenemos arg1 requerido por posición, arg2 es opcional por posición, luego arg3, 4, 5 y 6 son argumentos con nombre mezclados entre requeridos y opcionales (a diferencia de los posicionales, los argumentos con nombre opcionales no necesitan estar agrupados).
Importante: Los argumentos con nombre SIEMPRE van al final.
Otra forma de llamar a este método es agrupando los argumentos con nombre como un hash:
En Ruby, si el último argumento al llamar un método es un hash se puede escribir sin las llaves. A veces podemos ser explicitos y poner las llaves para que sea más fácil de leer.
Además, si queremos pasar un hash para el argumento arg1, necesitamos usar paréntesis al invocar el método:
Argumentos variables
Podemos hacer que nuestro método acepte una cantidad variable y desconocida de argumentos que no sepamos qué son!
Ahora foo acepta cualquier cantidad de argumentos que van a guardarse en el array que definimos como "args".
Esto es útil cuando queremos tener un método tipo "proxy" que acepte más argumentos que los que necesita, utilice algunos y pase el resto a otro método:
En este ejemplo, foo recibe cualquier cantidad de argumentos pero sólo usa el primero, el resto los usa para invocar el método bar.
También podemos aceptar una cantiadad variable de argumentos con nombre:
Y podemos combinar ambos casos:
Importante: Si nuestro método no define argumentos con nombre ni el argumento especial "**nombre", el último ejemplo funciona de forma diferente!
Al no agruparlo en un hash como **kargs al definir foo, los argumentos que antes eran kargs ahora son un elemento más de args.
Ruby 3 va a cambiar un poco el manejo de cómo separa los argumentos según la definición del método y cómo se pasan, en los casos que el funcionamiento sea diferente a Ruby 2.x se va a mostrar un warning.
Descomposición de arrays
También podemos indicar que nuestro método recibe un array como argumento y descomponer los elementos en variables:
Si no queremos perder el resto de elementos del array, podemos guardarlos también en otra variable:
Incluso podemos descomponer arrays anidados:
Veamos más en detalle:
- el argumento del método es un array que tiene otro array adentro y varios integers
- el array interno se descompone en el1 = 1, el2 = 2, resto1 = [3,4,5]
- el los siguientes elementos del array se descomponen en el3 = 6, resto2 = [7,8,9]
Ignorando argumentos
Puede ocurrir que nuestro método antes necesitaba argumentos y por algún cambio ahora no son necesarios pero no queremos ir por todo el código buscando dónde se usaba el método para cambiar el llamado. En ese caso podemos definir que se ignoren los argumentos usando el operador "*" como en el caso de argumentos variables pero sin dar un nombre al array:
Bloque de código
But wait... there's more! Nos queda otra opción más! Además de recibir datos, el método puede recibir un bloque de código. Este bloque siempre tiene que ser el último argumento:
Vayamos un poco más despacio con este ejemplo:
- foo recibe dos argumentos: el integer 1 y un bloque que imprime un valor que recibe como variable
- foo multiplica arg1 (1) por 2 y lo guarda en x (ahora x == 2)
- foo llama al método "call" del bloque con x como argumento
- nuestro bloque recibe el valor de x (2) en la variable a
- nuestro bloque imprime el valor de a
Veamos otro ejemplo un poco más complejo, no necesitamos definir el argumento &bloque si vamos a pasar el objeto actual como argumento:
Vamos a reescribir foo de forma explícita para que se parezca al ejemplo anterior:
Ahora es más fácil de seguir, pero ambos códigos son equivalentes y para casos donde se llama al bloque con el objeto actual es más común usar la opción sin "&bloque" y con "yield".
- El método foo recibe un integer y un bloque
- guarda el resultado de la multiplicación en una variable de instancia x
- llama al bloque con si mismo como argumento
- el bloque recibe el objeto en la variable a y llama al método x
Para terminar: combinando todo lo anterior!
Nuestro método acepta:
- pimera posición: requerido arg1
- segunda posición: un array que descompone en el1 y el resto
- tercera posición: requerido arg2
- cuarta posición: opcional arg3 cuyo valor por defecto es el valor de arg2 multiplicado por 2
- quinta posición en adelante: opcionales extra en array args
- después acepta varios argumentos con nombre, algunos requeridos (arg4 y arg6), uno opcional arg5 que su valor por defecto depende de un argumento posicional
- luego agrupa el resto de argumentos con nombre en el hash kargs
- al final recibe un bloque que es ejecutado dentro del método con un string hardcodeado
Bonus: "curry" o métodos parcialmente aplicados
Esta funcionalidad no es muy conocida, pero es interesante de comentar. Recordemos que en Ruby todo son objetos, incluso los métodos son objetos de la clase Method!!! y responden a otros métodos 😵... Entre los métodos de Method tenemos "curry". Lo que hace este método es generar un proc con uno de los argumentos ya reemplazados por un valor concreto, luego podemos usar ese proc para continuar evaluando el método. Esto es útil para tener métodos "generadores" de métodos:
Imaginemos este método que multiplica un número por un multiplicador:
Ahora supongamos que siempre queremos multiplicar por 3, entonces no queremos escribir siempre la primera parte. Podemos usar curry para genera un Proc donde va reemplazando las variables de a una en orden y cuando se tienen todos los argumentos necesarios ejecuta el código:
Lo que hicimos acá fue:
- obtenemos el objeto instancia de la clase Method que es el método que definimos antes
- llamamos al método curry
- evaluamos sólo el primer argumento
- guardamos el resultado en una variable por3
Luego podemos usar este proc para pasar el argumento que falta así se ejecuta el método:
Algo a tener en cuenta al usar este caso es que si nuestro método tiene argumentos opcionales, tenemos que indicarle a curry cuántos de los argumentos son requeridos, así sabe cuándo ejecutar el código:
Si no hacemos esto, siempre tendríamos que pasar un valor para el argumento c.
Conclusión
Ruby acepta muchas MUCHAS formas distintas de argumentos, de hecho hay más detalles de todo que no podría explicar porque son casos tan rebuscados que no terminaríamos más. Nos quedó afuera la sobreescritura de métodos y el llamado a super que se agregó un cambio respecto a esto en Ruby 2.7.También debe haber cosas que ni conozco y algunas cosas que están en desarrollo como el operador pipe que tienen lenguajes como F# o Erlang para concatenar llamadas a métodos (por ahora lo cancelaron después de varios intentos).
Recursos:
- Doc oficial: https://docs.ruby-lang.org/en/2.7.0/syntax/methods_rdoc.html#label-Arguments
- Clase Method: https://ruby-doc.org/core-2.7.0/Method.html
- Sobre "curry" y un caso de uso (entre otras cosas): https://www.youtube.com/watch?v=BV1-Z38ZWQU
Hoy vamos a ver algo diferente, que en principio puede parecer básico pero Ruby tiene muchas opciones para pasar argumentos a los métodos que resultan muy interesantes. Vamos a ir definiendo unos métodos agregando complejidad
¿Qué es un método?
Un método es un concepto asociado a la programación orientada a objetos (OOP), es similar a lo que en otros lenguajes se conoce como "función" pero, como en Ruby TODO son objetos, no hay "funciones" sino sólo "métodos".
Los métodos y funciones agrupan código para poder ser ejecutado más fácilmente sin necesidad de repetir ese código en los lugares que se necesite correr. La diferencia principal, conceptualmente hablando, es que los métodos se ejecutan en el contexto de un objeto instancia de una clase pudiendo cambiar su resultado según el estado de dicho objeto; en cambio las funciones se ejecutan como código aislado cuyo resultado sólo depende de los datos de entrada.
En algunos lenguajes como Ruby no tenemos el concepto de "función" porque todo son objetos, entonces siempre los bloques de código agrupados son métodos. En otros lenguajes ocurre lo opuesto, en Elixir, por ejemplo, no existen los objetos, por lo que no existen los métodos, los bloques de código agrupado son siempre funciones. También hay lenguajes que soportan ambos conceptos: tanto funciones aisladas como objetos con métodos. En esos lenguajes, las funciones y los métodos se suelen definir de la misma forma y cambia su significado según el contexto (si defino una función dentro de la declaración de un objeto, esta es conceptualmente un método de ese objeto).
Si en Ruby definimos un método que en principio podría parecer aislado, este se define como un método del objeto main, por lo que no es una función sino un bloque de código que se ejecuta en el contexto de un objeto.
¿Qué son los argumentos?
Un método puede o no necesitar datos externos para ejecutar su código. Un método que no necesita datos externos podría ser por ejemplo este:
def pi
3.14
end
Recordemos que en Ruby no necesitamos la palabra clave "return", un método siempre devuelve el resultado de la última línea de código si no le indicamos otra cosa.
Un método que sí necesite datos podría leerlos directamente del objeto al que pertenece o recibir datos al ser invocado. Como no estamos tratando de enseñar conceptos de OOP vamos a hablar sólo de los argumentos que se pasan al invocar un método. Recién vimos el método "pi", este método tiene 0 argumentos (la cantidad de argumentos se llama "aridad").
Vamos a agregar entonces argumentos a una función y vamos a simplificarla para mostrar lo que hace cada argumento en lugar de mostrar un resultado fijo como el valor de Pi.
Argumentos requeridos
El primer ejemplo es simple, un método que tiene 1 (o más) argumento requeridos:
def foo(arg1)
puts "arg1 es: #{arg1.inspect}"
end
Este método recibe un dato e imprime el nombre de la variable que asignamos y su valor.
> foo(1)
arg1 es: 1
Para invocar el método foo si o si le tenemos que pasar un argumento, si no lo hacemos da error:
> foo
ArgumentError (wrong number of arguments (given 0, expected 1))
Podemos tener la cantidad de argumento que queramos:
def foo(arg1, arg2, arg3, arg4, arg5) # y podríamos seguir
Si bien se puede, está considerado mala práctica o un "code smell" tener demasiados argumentos porque el código se vuelve más complejo. Se puede, pero hay que hacerlo con criterio. En algunas guías de estilo dicen que 5 debería ser el máximo de argumentos de una función.
Argumentos opcionales
Si queremos que nuestro método acepte un argumento pero que no sea requerido, podemos asignarle un valor por defecto:
def foo(arg1 = 'por defecto')
puts "arg1 es: #{arg1.inspect}"
end
Entonces:
> foo(1)
arg1 es: 1
> foo
arg1 es: "por defecto"
Combinando argumentos requeridos y opcionales
Ahora podemos agregar más argumentos y combinar requeridos y combinados de distintas formas:
def foo(arg1, arg2 = 'por defecto')
puts "arg1 es: #{arg1.inspect}"
puts "arg2 es: #{arg2.inspect}"
end
> foo(1)
arg1 es: 1
arg2 es: "por defecto"> foo(1,2)
arg1 es: 1
arg2 es: 2
En algunos lenguajes necesitamos que los argumentos opcionales estén al final, en Ruby no.
def foo(arg1 = 'por defecto1', arg2 = 'por defecto2', arg3, arg4)
puts "arg1 es: #{arg1.inspect}"
puts "arg2 es: #{arg2.inspect}"
puts "arg3 es: #{arg3.inspect}"
puts "arg4 es: #{arg4.inspect}"
end
Acá se vuelve un poco confuso... Si la cantidad de argumentos que usamos al invocar al método es igual al total no pasa nada raro:
> foo(1, 2, 3, 4)
arg1 es: 1
arg2 es: 2
arg3 es: 3
arg4 es: 4
Pero si es menor que el total, primero se asignan los argumentos requeridos con el final de los que indicamos y luego los opcionales con los primeros que indicamos. Un ejemplo es más claro:
> foo(1,2)
arg1 es: "por defecto1"
arg2 es: "por defecto2"
arg3 es: 1
arg4 es: 2
> foo(1,2,3)
arg1 es: 1
arg2 es: "por defecto2"
arg3 es: 2
arg4 es: 3
Es importante que los argumentos opcionales estén agrupados (no es necesario agrupar los requeridos):
def foo(arg1, arg2 = 'por defecto1', arg3 = 'por defacto2', arg4) # es válido
def foo(arg1, arg2 = 'por defecto1', arg3, arg4 = 'por defecto2') # error de sintaxis!!
Argumento opcional con valor por defecto dependiente de otro argumento
Podemos asignar como valor por defecto de un argumento, una transformación de otro argumento!
def foo(arg1 = 'por defecto', arg2 = arg1 * 2, arg3)
puts "arg1 es: #{arg1.inspect}"
puts "arg2 es: #{arg2.inspect}"
puts "arg3 es: #{arg3.inspect}"
end
> foo(1)
arg1 es: "por defecto"
arg2 es: "por defectopor defecto" # el valor de arg2 es el de arg1 multiplicado por 2
arg3 es: 1
Incluso podemos usar el resultado de otro método usando el valor de otro argumento:
def multi(x)
x * 2
end
def foo(arg1 = 'por defecto', arg2 = multi(arg1), arg3)
puts "arg1 es: #{arg1.inspect}"
puts "arg2 es: #{arg2.inspect}"
puts "arg3 es: #{arg3.inspect}"
end
El único requerimiento para que esto funcione es que el argumento utilizado tiene que estar definido a la izquierda del que lo usa. Esto no es válido:
def foo(arg1 = 'por defecto', arg2 = multi(arg3), arg3)
En este caso, "arg3" aún no está definido al llamar el método y Ruby intenta buscar un método "arg3".
Argumentos con nombre (keyword arguments)
A veces es dificil recordar el orden de los argumentos posicionales. Para evitar este problema se pueden definir argumentos con nombre (o keyword arguments) que, a diferencia de los posicionales, no tienen que ponerse en ningún orden en particular al invocar el método. Cambia un poco la sintaxis:
def foo(arg1:, arg2: 'por defecto')
puts "arg1 es: #{arg1.inspect}"
puts "arg2 es: #{arg2.inspect}"
end
Los argumentos requeridos ahora terminan en ":" y los opcionales no llevan el "=". Ya no podemos invocar el método con foo(1) porque Ruby espera que los argumentos indiquen el nombre del argumento al que pertenecen en lugar de usar su posición. Entonces tenemos que hacer:
> foo(arg1: 1)
arg1 es: 1
arg2 es: "por defecto"
> foo(arg1: 1, arg2: 2)
arg1 es: 1
arg2 es: 2
> foo(arg2: 2, arg1: 1) # podemos usar el orden que queramos
arg1 es: 2
arg2 es: 1
> foo
ArgumentError (missing keyword: arg1)
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"
En este ejemplo tenemos muchos conceptos mezclados: tenemos arg1 requerido por posición, arg2 es opcional por posición, luego arg3, 4, 5 y 6 son argumentos con nombre mezclados entre requeridos y opcionales (a diferencia de los posicionales, los argumentos con nombre opcionales no necesitan estar agrupados).
Importante: Los argumentos con nombre SIEMPRE van al final.
Otra forma de llamar a este método es agrupando los argumentos con nombre como un hash:
> foo(1, {arg3: 3, arg5: 5}) # es exactamente igual al anterior, los { } son opcionales
arg1 es: 1
arg2 es: "por defecto"
arg3 es: 3
arg4 es: "por defecto4"
arg5 es: 5
arg6 es: "por defecto6"
En Ruby, si el último argumento al llamar un método es un hash se puede escribir sin las llaves. A veces podemos ser explicitos y poner las llaves para que sea más fácil de leer.
Además, si queremos pasar un hash para el argumento arg1, necesitamos usar paréntesis al invocar el método:
> foo(1, arg3: 3, arg5: 5) # funciona
> foo 1, arg3: 3, arg5: 5 # funciona sin ( )
> foo({arg6: 1}, arg3: 3, arg5: 5) # funciona
> foo {arg6: 1}, arg3: 3, arg5: 5 # error de sintaxis
Argumentos variables
Podemos hacer que nuestro método acepte una cantidad variable y desconocida de argumentos que no sepamos qué son!
def foo(*args)
puts "args es: #{args}"
end
> foo(1)
args es: 1
> foo(1,2)
args es: [1, 2]
Ahora foo acepta cualquier cantidad de argumentos que van a guardarse en el array que definimos como "args".
Esto es útil cuando queremos tener un método tipo "proxy" que acepte más argumentos que los que necesita, utilice algunos y pase el resto a otro método:
def bar(arg2, *args)
puts "arg2 en bar es: #{arg2}"
puts "args en bar es: #{args}"
end
def foo(arg1, *args)
puts "arg1 en foo es: #{arg1}"
bar(*args)
end
> foo(1,2,3,4)
arg1 en foo es: 1
arg2 en bar es: 2
args en bar es: [3, 4]
En este ejemplo, foo recibe cualquier cantidad de argumentos pero sólo usa el primero, el resto los usa para invocar el método bar.
También podemos aceptar una cantiadad variable de argumentos con nombre:
def foo(**kargs)
puts "kargs es: #{kargs}"
end
> foo(x: 1, y: 2) # equivalente a foo({x: 1, y: 2})
kargs es: {:x => 1, :y => 2}
Y podemos combinar ambos casos:
def foo(*args, **kargs)
puts "args es: #{args}"
puts "kargs es: #{kargs}"
end
> foo(1,2) # al no tener nombre los agrupa como posicionales
args es: [1, 2]
kargs es: {}
> foo(x: 1, y: 2) # al tener nombre los agrupa como keywords
args es: []
kargs es: {:x => 1, :y => 2}
> foo(1, 2, x: 1, y: 2) # equivalente a foo(1, 2, {x: 1, y: 2})
args es: [1, 2]
kargs es: {:x => 1, :y => 2}
Importante: Si nuestro método no define argumentos con nombre ni el argumento especial "**nombre", el último ejemplo funciona de forma diferente!
def foo(*args)
puts "args es: #{args}"
end
> foo(1, x: 1, y: 2)
args es: [1, {:x => 1, :y => 2}]
Al no agruparlo en un hash como **kargs al definir foo, los argumentos que antes eran kargs ahora son un elemento más de args.
Ruby 3 va a cambiar un poco el manejo de cómo separa los argumentos según la definición del método y cómo se pasan, en los casos que el funcionamiento sea diferente a Ruby 2.x se va a mostrar un warning.
Descomposición de arrays
También podemos indicar que nuestro método recibe un array como argumento y descomponer los elementos en variables:
def foo((el1, el2))
puts "el1 es: #{el1}"
puts "el2 es: #{el2.inspect}"
end
> foo([1,2])
el1 es: 1
el2 es: 2
> foo([1]) # lo que no definamos es nil, no podemos definir un default en este caso
el1 es: 1
el2 es: nil
> foo(1) # si el array tiene un solo elemento, podemos pasarlo sin [ ]
el1 es: 1
el2 es: nil
> foo([1,2,3]) # se pierde cualquier elemento que definamos de más
el1 es: 1
el2 es: 2
Si no queremos perder el resto de elementos del array, podemos guardarlos también en otra variable:
def foo((el1, el2, *resto))
puts "el1 es: #{el1}"
puts "el2 es: #{el2.inspect}"
puts "resto es: #{resto.inspect}"
end
> foo([1,2,3,4])
el1 es: 1
el2 es: 2
resto es: [3, 4]
Incluso podemos descomponer arrays anidados:
def foo(((el1, el2, *resto1), el3, *resto2))
puts "el1 es: #{el1}"
puts "el2 es: #{el2}"
puts "el3 es: #{el3}"
puts "resto1 es: #{resto1}"
puts "resto2 es: #{resto2}"
end
> foo([[1,2,3,4,5],6,7,8,9])
el1 es: 1
el2 es: 2
el3 es: 6
resto1 es: [3, 4, 5]
resto2 es: [7, 8, 9]
Veamos más en detalle:
- el argumento del método es un array que tiene otro array adentro y varios integers
- el array interno se descompone en el1 = 1, el2 = 2, resto1 = [3,4,5]
- el los siguientes elementos del array se descomponen en el3 = 6, resto2 = [7,8,9]
Ignorando argumentos
Puede ocurrir que nuestro método antes necesitaba argumentos y por algún cambio ahora no son necesarios pero no queremos ir por todo el código buscando dónde se usaba el método para cambiar el llamado. En ese caso podemos definir que se ignoren los argumentos usando el operador "*" como en el caso de argumentos variables pero sin dar un nombre al array:
def foo(arg1, *) # usa el primer argumento e ignora el resto
def foo(arg1, *resto, **) # usa el primer argumento, guarda el resto de argumentos posicionales en un array e ignora el resto de argumentos con nombre
def foo(arg1, *, **kargs) #usa el primer argumento, ignora el resto de argumentos posicionales y guarda los argumentos con nombre en kargs
Bloque de código
But wait... there's more! Nos queda otra opción más! Además de recibir datos, el método puede recibir un bloque de código. Este bloque siempre tiene que ser el último argumento:
def foo(arg1, &bloque)
x = arg1 * 2
bloque.call(x)
end
> foo(1) do |a| # equivalente a foo 1 do |a|
puts a
end
2
Vayamos un poco más despacio con este ejemplo:
- foo recibe dos argumentos: el integer 1 y un bloque que imprime un valor que recibe como variable
- foo multiplica arg1 (1) por 2 y lo guarda en x (ahora x == 2)
- foo llama al método "call" del bloque con x como argumento
- nuestro bloque recibe el valor de x (2) en la variable a
- nuestro bloque imprime el valor de a
Veamos otro ejemplo un poco más complejo, no necesitamos definir el argumento &bloque si vamos a pasar el objeto actual como argumento:
class Bar
attr_accessor :x
def foo(arg1)
self.x = arg1 * 2
yield self
end
end
> mi_bar = Bar.new
> mi_bar.foo(1) do |a|
puts a.x
end
2
Vamos a reescribir foo de forma explícita para que se parezca al ejemplo anterior:
class Bar
attr_accessor :x
def foo(arg1, &bloque)
self.x = arg1 * 2
bloque.call(self)
end
end
Ahora es más fácil de seguir, pero ambos códigos son equivalentes y para casos donde se llama al bloque con el objeto actual es más común usar la opción sin "&bloque" y con "yield".
- El método foo recibe un integer y un bloque
- guarda el resultado de la multiplicación en una variable de instancia x
- llama al bloque con si mismo como argumento
- el bloque recibe el objeto en la variable a y llama al método x
Para terminar: combinando todo lo anterior!
def foo(arg1, (el1, *resto), arg2, arg3 = arg2 * 2, *args, arg4:, arg5: arg1 * 2, arg6:, **kargs, &bloque)
puts "arg1 es: #{arg1}"
puts "el1 es: #{el1}"
puts "resto es: #{resto}"
puts "arg2 es: #{arg2}"
puts "arg3 es: #{arg3}"
puts "args es: #{args}"
puts "arg4 es: #{arg4}"
puts "arg5 es: #{arg5}"
puts "arg6 es: #{arg6}"
puts "kargs es: #{kargs}"
bloque.call("llamo al bloque dentro de foo")
end
> foo(1, ['a', 'b', 'c'], 2, 3, 'baz', arg4: 4, arg6: 6, arg7: 7) do |a|
puts a
end
arg1 es: 1
el1 es: a
resto es: ["b", "c"]
arg2 es: 2
arg3 es: 3
args es: ["baz"]
arg4 es: 4
arg5 es: 2
arg6 es: 6
kargs es: {:arg7=>7}
llamo al bloque dentro de foo
Nuestro método acepta:
- pimera posición: requerido arg1
- segunda posición: un array que descompone en el1 y el resto
- tercera posición: requerido arg2
- cuarta posición: opcional arg3 cuyo valor por defecto es el valor de arg2 multiplicado por 2
- quinta posición en adelante: opcionales extra en array args
- después acepta varios argumentos con nombre, algunos requeridos (arg4 y arg6), uno opcional arg5 que su valor por defecto depende de un argumento posicional
- luego agrupa el resto de argumentos con nombre en el hash kargs
- al final recibe un bloque que es ejecutado dentro del método con un string hardcodeado
Bonus: "curry" o métodos parcialmente aplicados
Esta funcionalidad no es muy conocida, pero es interesante de comentar. Recordemos que en Ruby todo son objetos, incluso los métodos son objetos de la clase Method!!! y responden a otros métodos 😵... Entre los métodos de Method tenemos "curry". Lo que hace este método es generar un proc con uno de los argumentos ya reemplazados por un valor concreto, luego podemos usar ese proc para continuar evaluando el método. Esto es útil para tener métodos "generadores" de métodos:
Imaginemos este método que multiplica un número por un multiplicador:
def multi(multiplicador, numero)
multiplicador * numero
end
> multi(3, 5)
15
Ahora supongamos que siempre queremos multiplicar por 3, entonces no queremos escribir siempre la primera parte. Podemos usar curry para genera un Proc donde va reemplazando las variables de a una en orden y cuando se tienen todos los argumentos necesarios ejecuta el código:
> por3 = method(:multi).curry.call(3)
Lo que hicimos acá fue:
- obtenemos el objeto instancia de la clase Method que es el método que definimos antes
- llamamos al método curry
- evaluamos sólo el primer argumento
- guardamos el resultado en una variable por3
Luego podemos usar este proc para pasar el argumento que falta así se ejecuta el método:
> por3.call(5) # equivalente a por3.(5) (es un shortcut de ".call"
15
> por3.call(6)
18
Algo a tener en cuenta al usar este caso es que si nuestro método tiene argumentos opcionales, tenemos que indicarle a curry cuántos de los argumentos son requeridos, así sabe cuándo ejecutar el código:
def foo(a, b, c = 1)
end
> aux = method(:foo).curry(2)
Si no hacemos esto, siempre tendríamos que pasar un valor para el argumento c.
Conclusión
Ruby acepta muchas MUCHAS formas distintas de argumentos, de hecho hay más detalles de todo que no podría explicar porque son casos tan rebuscados que no terminaríamos más. Nos quedó afuera la sobreescritura de métodos y el llamado a super que se agregó un cambio respecto a esto en Ruby 2.7.También debe haber cosas que ni conozco y algunas cosas que están en desarrollo como el operador pipe que tienen lenguajes como F# o Erlang para concatenar llamadas a métodos (por ahora lo cancelaron después de varios intentos).
Recursos:
- Doc oficial: https://docs.ruby-lang.org/en/2.7.0/syntax/methods_rdoc.html#label-Arguments
- Clase Method: https://ruby-doc.org/core-2.7.0/Method.html
- Sobre "curry" y un caso de uso (entre otras cosas): https://www.youtube.com/watch?v=BV1-Z38ZWQU
Comentarios
Publicar un comentario