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 muchísimo en Crystal, por ejemplo para iteraciones y callbacks. Pero también 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, tienes que añadir un ampersand (&) antes del último argumento.
Esto convertirá el bloque en un Proc y lo asignará al argumento.
Después, usar yield ejecutará el bloque que se le ha pasado 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 ha pasado 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 el número de argumentos que se pueden pasar al bloque.
Para usar los argumentos en el bloque, se utiliza la sintaxis |x, y, ...|.
Aquí, 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 pasa el argumento 2, y el bloque lo multiplica por 3 y devuelve 6.
Después, el método suma 1 al resultado y devuelve 7.
Si el número de argumentos que se le dan a yield es mayor que el número de argumentos que espera el bloque, se lanzará 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 aparecen en los ejemplos anteriores. Incluso puedes dar al bloque argumentos de distintos tipos, 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 devuelto.
Esto se hace con la sintaxis Arg1Type, Arg2Type, ... -> ResultType.
Ahí, los argumentos se separan por 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
Al igual que con otros tipos explícitos, el compilador hará cumplir que los tipos sean los esperados. Si el bloque no coincide con el tipo dado al método, se lanzará 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 & delante 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 sitio a otro como una variable.
Para definir un Proc se utiliza la sintaxis del literal de proc ->(args) { ... }.
Los argumentos se separan por comas (,) y el bloque se separa mediante { ... }.
Para invocar un Proc se utiliza el método call, pasándole los argumentos.
Los tipos de los parámetros son obligatorios.
El tipo devuelto 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 resultar ú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
Al declarar 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 será siempre el tipo devuelto, y el resto serán los tipos de los argumentos.
Es decir, si solo hay un tipo, ese será el tipo devuelto y el proc no tomará ningún argumento.
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)