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 del dominio); algunos ejemplos de DSL son el framework web Kemal o el framework de pruebas Spec (que es lo que se usa para probar tus soluciones en 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.
Después, usar yield ejecutará el bloque que se le pasó al método.
def my_method(&block)
1 + yield
end
Para pasar un bloque hay dos formas: con las palabras clave do ... end o con 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 pasó 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 se usa 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, 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 da a yield es mayor que la cantidad de argumentos que espera el bloque, se producirá 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 hará que los tipos sean los esperados. Si el bloque no coincide con el tipo que se le dio al método, se producirá un error.
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 se usa la sintaxis de literal de proc ->(args) { ... }.
Los argumentos se separan con comas (,), y el bloque se separa con { ... }.
Para invocar un Proc se usa el método call y se le pasan 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, se usa 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, este 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 }
Estás desarrollando un sistema de gestión de contenido para un sitio web que aloja artículos, entradas de blog y otro contenido escrito. Se te encargó implementar un manejador de tareas que procese tareas en una cola de prioridad. El manejador de tareas recibirá tareas y, según la carga del servidor y su prioridad, decidirá cuál tarea ejecutar a continuación. Si el servidor está bajo mucha carga, el número de prioridad tiene que ser más alto para que la tarea se ejecute. El servidor cambiará la carga dinámicamente según la carga.
TaskHandler
El TaskHandler se debe inicializar con una cola y un load_level predeterminado.
Implementa el inicializador de la clase TaskHandler, que recibe como argumento un Array de Strings con los nombres de las tareas y lo guarda en una variable de instancia llamada @tasks.
El inicializador también debe crear la variable de instancia @task_condition_logic, que guarda un Proc que recibe un Int32 y devuelve true si el argumento es igual o mayor que 0, y false si es menor que 0.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
# => #<TaskHandler:0x7fd8d4cfbf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562536a4a330>>
El servidor tiene que poder actualizar la lógica de la condición de tareas según la carga del servidor.
Implementa el método TaskHandler#update_task_condition_logic, que recibe un bloque como argumento, y ese bloque se debe guardar en la variable de instancia @task_condition_logic.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
task_handler.update_task_condition_logic { |load| load > 1 }
# => #<TaskHandler:0x7f068118cf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562df99d0370>>
El servidor tiene que poder ejecutar la siguiente tarea de la cola.
La primera tarea del array @tasks es la siguiente tarea por ejecutar.
La tarea se debe ejecutar si el nivel de prioridad de la tarea dada devuelve true al pasarlo al proc @task_condition_logic.
Devolverá mensajes según si la tarea se ejecutó o no.
Si la tarea se ejecutó, debe devolver: "Completed <task>".
Si la tarea no se ejecutó, debe devolver: "Could not complete <task>".
Si la tarea se ejecutó, se debe eliminar del array @tasks.
Implementa el método TaskHandler#execute_next_task, que recibe como argumento el nivel de prioridad de la siguiente tarea como un Int32 y devuelve un String según si la tarea se ejecutó o no.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
task_handler.update_task_condition_logic { |load| load > 1 }
task_handler.execute_next_task(2)
# => "Completed task1"
task_handler.execute_next_task(1)
# => "Could not complete task2"
Regístrate en Exercism para aprender y dominar Crystal con 26 conceptos142 ejercicios y mentoría humana real, todo gratis.