Rutas
/
Crystal
Crystal
/
Ejercicios
/
Gestor de tareas
Gestor de tareas

Gestor de tareas

Ejercicio de aprendizaje

Introducción

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 cosas como la iteración y los 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).

Bloques

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

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 dentro del bloque se utiliza 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. 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)

Tipos de bloques

Los bloques se pueden escribir de forma explícita o implícita. Los implícitos son los más comunes y son los que aparecen en los ejemplos anteriores. Incluso puedes darle 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

Como ocurre con otros tipos explícitos, el compilador exigirá que los tipos sean los esperados. Si el bloque no coincide con el tipo que se le ha dado al método, se lanzará un error.

Procs

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 utiliza la sintaxis del literal de proc ->(args) { ... }. Los argumentos se separan por comas (,) y el bloque se delimita 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 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

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

Pasar procs a métodos

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

Declarar el tipo de las variables

Al declarar una variable puedes usar la sintaxis : Type para declarar su tipo. Para los procs, se usa 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 recibirá ningún argumento.

my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }

Instrucciones

Estás desarrollando un sistema de gestión de contenidos para un sitio web que aloja artículos, entradas de blog y otros contenidos escritos. Se te ha encargado implementar un gestor de tareas que procese tareas en una cola de prioridad. El gestor de tareas recibirá tareas y, según la carga del servidor y su prioridad, decidirá qué tarea ejecutar a continuación. Si el servidor está bajo una carga elevada, el número de prioridad tendrá que ser mayor para que la tarea se ejecute. El servidor cambiará dinámicamente la carga en función de la carga.

1. Inicializa TaskHandler

El TaskHandler debe inicializarse con una cola y un load_level predeterminado.

Implementa el inicializador de la clase TaskHandler, que recibe como argumento un Array de String con los nombres de las tareas y lo almacena en una variable de instancia llamada @tasks. El inicializador también debe crear la variable de instancia @task_condition_logic, que almacena 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>>

2. Actualiza la lógica de condiciones de las tareas

El servidor tiene que poder actualizar la lógica de condiciones de las tareas en función de la carga del servidor.

Implementa el método TaskHandler#update_task_condition_logic, que recibe un bloque como argumento; ese bloque debe almacenarse 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>>

3. Ejecuta la siguiente tarea

El servidor tiene que poder ejecutar la siguiente tarea de la cola.

La primera tarea del array @tasks es la siguiente tarea que se va a ejecutar. La tarea debe ejecutarse 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ó, debe eliminarse del array @tasks.

Implementa el método TaskHandler#execute_next_task, que recibe como argumento el nivel de prioridad como Int32 de la siguiente tarea 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"
Editar en GitHub El enlace se abre en una ventana o pestaña nueva
Crystal Exercism

¿Listo para empezar Gestor de tareas?

Regístrate en Exercism para aprender y dominar Crystal con 26 conceptos142 ejercicios y mentoría humana real, todo gratis.