Percursos
/
Crystal
Crystal
/
Exercícios
/
Gestor de Tarefas
Gestor de Tarefas

Gestor de Tarefas

Exercício de aprendizagem

Introdução

Um bloco é um pedaço de código que pode ser passado a um método. Noutras linguagens de programação, isto é conhecido como função anónima ou lambda. Um método pode converter um bloco num Proc, que representa um bloco de código.

def my_method(&block)
  1 + yield
end

my_method { 2 }
# => 3

Blocos e Procs são muito usados em Crystal, por exemplo para iteração e callbacks. Mas também para criar DSLs (linguagens específicas de domínio); alguns exemplos de DSLs são o framework web Kemal ou o framework de testes Spec (que é o que se usa para testar as tuas soluções em Crystal).

Blocos

Para criar um método que aceita um bloco, tens de acrescentar um e comercial (&) antes do último argumento. Isto converte o bloco num Proc e atribui-o ao argumento. Depois, usar yield executa o bloco que foi dado ao método.

def my_method(&block)
  1 + yield
end

Há duas formas de passar um bloco: com as palavras-chave do ... end ou com chavetas ({}). A principal diferença entre as duas é a precedência: o do ... end suporta blocos com várias linhas, enquanto as chavetas ({}) não.

my_method do
  2
end
# => 3

my_method { 2 }
# => 3

Yield

yield é uma palavra-chave que executa o bloco dado ao método. Isto significa que o bloco nunca será executado se o yield não for chamado. Também se lhe podem dar argumentos, que são passados ao bloco. Não há limite para o número de argumentos que podem ser passados ao bloco. Para usares os argumentos no bloco, usas a sintaxe |x, y, ...|. Aqui, x, y, ... são os nomes dos 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

No exemplo acima, o bloco recebe o argumento 2, multiplica-o por 3 e devolve 6. Depois, o método soma 1 ao resultado e devolve 7.

Se o número de argumentos dados ao yield for maior do que o número de argumentos que o bloco espera, é lançado um erro.

def my_method(&block)
  1 + yield
end

my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)

Tipos de blocos

Os blocos podem ser escritos de forma explícita ou implícita. Os blocos implícitos são os mais comuns e são os que aparecem nos exemplos acima. Podes até dar ao bloco argumentos de tipos diferentes: o compilador infere os tipos e cria um tipo de união.

def my_method(&block)
  yield "a"
  yield 1
end

my_method do |x|
  p typeof(x)
end

# output: (String | Int32)

Ao escreveres blocos explícitos, podes especificar os tipos dos argumentos e o tipo devolvido. Faz-se isso com a sintaxe Arg1Type, Arg2Type, ... -> ResultType. Aí, os argumentos são separados por vírgulas (,) e o tipo do resultado é separado por ->.

def my_method(&block : Int32, Int32 -> Int32)
  1 + yield(2, 3)
end

my_method { |x, y| x * y }
# => 7

Tal como acontece com outros tipos explícitos, o compilador verifica se os tipos são os esperados. Se o bloco não corresponder ao tipo indicado no método, é lançado um erro.

Procs

O Proc tem uma ligação muito forte aos blocos. A diferença essencial é que um Proc é um tipo e um bloco não. Um Proc representa um ponteiro para função e pode ser passado como uma variável.

Para definires um Proc, usas a sintaxe do literal de proc ->(args) { ... }. Os argumentos são separados por vírgulas (,) e o bloco por { ... }. Para invocares um Proc, usas o método call e passas-lhe os argumentos. Os tipos dos parâmetros são obrigatórios. O tipo devolvido é inferido a partir do corpo do proc, mas também pode ser indicado de forma explícita.

my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6

Capturar blocos

Capturar blocos é uma forma de criar um Proc a partir de um bloco. Faz-se isso com o operador & antes do último argumento de um método. Isso converte o bloco num Proc e atribui-o ao argumento.

def my_method(&block)
  block.call
end

my_method { 1 + 2 }
# => 3

Passar procs a métodos

Os procs podem ser passados a métodos que esperam um bloco. Isto pode ser útil quando queres poder passar blocos diferentes consoante uma condição.

def my_method(&block)
  yield 5
end

my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10

Declarar o tipo das variáveis

Ao declarares uma variável, podes usar a sintaxe : Type para declarar o tipo da variável. Para os procs, usas a sintaxe Proc((ArgType, ...), ResultType). O último tipo é sempre o tipo devolvido e os restantes são os tipos dos argumentos. Ou seja, se houver apenas um tipo, este é o tipo devolvido e o proc não recebe argumentos.

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

Instruções

Estás a desenvolver um sistema de gestão de conteúdos para um site que aloja artigos, publicações de blogue e outros conteúdos escritos. Foi-te pedido que implementes um gestor de tarefas que vai processar tarefas numa fila de prioridades. O gestor de tarefas recebe tarefas e, com base na carga do servidor e na prioridade delas, decide qual é a próxima tarefa a executar. Se o servidor estiver com uma carga elevada, o número de prioridade tem de ser mais alto para que a tarefa seja executada. O servidor vai alterar dinamicamente a carga com base na carga.

1. Inicializa o TaskHandler

O TaskHandler tem de ser inicializado com uma fila e um load_level predefinido.

Implementa o inicializador da classe TaskHandler, que recebe um Array de Strings com os nomes das tarefas como argumento e o guarda numa variável de instância chamada @tasks. O inicializador deve também criar a variável de instância @task_condition_logic, que guarda um Proc que recebe um Int32 e devolve true se o argumento for igual ou superior a 0 e false se for inferior a 0.

task_handler = TaskHandler.new(["task1", "task2", "task3"])
# => #<TaskHandler:0x7fd8d4cfbf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562536a4a330>>

2. Atualiza a lógica da condição das tarefas

O servidor tem de conseguir atualizar a lógica da condição das tarefas com base na carga do servidor.

Implementa o método TaskHandler#update_task_condition_logic, que recebe um bloco como argumento, e o bloco deve ser guardado na variável de instância @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. Executa a próxima tarefa

O servidor tem de conseguir executar a próxima tarefa da fila.

A primeira tarefa do array @tasks é a próxima tarefa a executar. A tarefa deve ser executada se o nível de prioridade da tarefa devolver true quando lhe for passado o proc @task_condition_logic.

Devolve mensagens consoante a tarefa tenha sido executada ou não. Se a tarefa foi executada, deve devolver: "Completed <task>". Se a tarefa não foi executada, deve devolver: "Could not complete <task>".

Se a tarefa foi executada, deve ser removida do array @tasks.

Implementa o método TaskHandler#execute_next_task, que recebe como argumento o nível de prioridade da próxima tarefa como Int32 e devolve um String consoante a tarefa tenha sido executada ou não.

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 via GitHub A ligação abre numa nova janela ou separador
Crystal Exercism

Estás pronto para começar Gestor de Tarefas?

Inscreve-te no Exercism para aprenderes e dominares Crystal com 26 conceitos142 exercícios, e mentoria humana real, tudo grátis.