Un bloque es un fragmento de código que se puede pasar a un método. En otros lenguajes de programación esto se conoce como función anónima o lambda. Un método puede convertir un bloque en un Proc, que representa un bloque de código.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
Los bloques y los procs se usan mucho en Crystal, por ejemplo para iteraciones y callbacks. Pero también sirven para crear DSL (lenguajes específicos de dominio); algunos ejemplos de DSL son el framework web Kemal o el framework de pruebas Spec (que es el que se usa para probar tus soluciones de Crystal).
Para crear un método que pueda aceptar un bloque, debes agregar un ampersand (&) antes del último argumento.
Esto convertirá el bloque en un Proc y lo asignará al argumento.
Luego, usar yield ejecutará el bloque que se le dio al método.
def my_method(&block)
1 + yield
end
Para pasar un bloque hay dos formas: usar las palabras clave do ... end o usar llaves ({}).
La principal diferencia entre ambas es la precedencia: do ... end admite bloques de varias líneas, mientras que las llaves ({}) no.
my_method do
2
end
# => 3
my_method { 2 }
# => 3
yield es una palabra clave que ejecuta el bloque que se le da al método.
Eso significa que el bloque nunca se ejecutará si no se llama a yield.
También se le pueden dar argumentos, que se pasarán al bloque.
No hay límite en la cantidad de argumentos que se pueden pasar al bloque.
Para usar los argumentos en el bloque, usas la sintaxis |x, y, ...|.
Ahí, x, y, ... son los nombres de los argumentos.
def my_method(&block)
1 + yield 2
end
# With curly braces:
my_method { |x| x * 3 }
# => 7
# With do ... end:
my_method do |x|
x * 3
end
# => 7
En el ejemplo anterior, al bloque se le da el argumento 2, y el bloque lo multiplica por 3 y devuelve 6.
Luego el método suma 1 al resultado y devuelve 7.
Si la cantidad de argumentos que se le dan a yield es mayor que la cantidad de argumentos que el bloque espera, se generará un error.
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
Los bloques se pueden escribir de forma explícita o implícita. Los bloques implícitos son los más comunes y son los que se muestran en los ejemplos anteriores. Incluso puedes darle al bloque distintos tipos de argumentos, y el compilador inferirá los tipos y creará un tipo unión.
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
Cuando escribes bloques explícitos, puedes especificar los tipos de los argumentos y el tipo de retorno.
Esto se hace con la sintaxis Arg1Type, Arg2Type, ... -> ResultType.
Ahí, los argumentos se separan con comas (,) y el tipo del resultado se separa con ->.
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
Como con otros tipos explícitos, el compilador verificará que los tipos sean los esperados. Si el bloque no coincide con el tipo que se le dio al método, se generará un error.
Existe una forma abreviada para los bloques que solo toman un argumento y solo llaman a un método sobre él.
En lugar de usar |x|, puedes usar & antes del nombre del argumento.
def my_method(&block)
yield "a"
end
# This is:
my_method { |x| x.upcase }
# => "A"
# Is the same as:
my_method &.upcase
# => "A"
Proc tiene una conexión muy fuerte con los bloques.
La diferencia clave es que un Proc es un tipo y un bloque no.
Un Proc representa un puntero a función y se puede pasar de un lado a otro como una variable.
Para definir un Proc, usas la sintaxis del literal de proc ->(args) { ... }.
Los argumentos se separan con comas (,) y el bloque se separa con { ... }.
Para invocar un Proc, usas el método call y le pasas los argumentos.
Los tipos de los parámetros son obligatorios.
El tipo de retorno se infiere del cuerpo del proc, pero también se puede indicar de forma explícita.
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
Capturar bloques es una forma de crear un Proc a partir de un bloque.
Esto se hace usando el operador & antes del último argumento de un método.
Eso convertirá el bloque en un Proc y lo asignará al argumento.
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
Los procs se pueden pasar a métodos que esperan un bloque. Esto puede ser útil cuando quieres poder pasar bloques distintos según alguna condición.
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
Cuando declaras una variable, puedes usar la sintaxis : Type para declarar el tipo de la variable.
Para los procs, usas la sintaxis Proc((ArgType, ...), ResultType).
El último tipo siempre será el tipo de retorno y el resto serán los tipos de los argumentos.
Es decir, si solo hay un tipo, ese será el tipo de retorno y el proc no tomará argumentos.
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
También puedes declarar un proc a partir de un método.
def my_method(a, b)
a + b
end
my_proc = ->my_method(Int32, Int32)
my_proc.call(2, 3)