トラック
/
Crystal
Crystal
/
演習
/
タスクハンドラー
タスクハンドラー

タスクハンドラー

学習演習

はじめに

ブロックは、メソッドに渡すことができるコードのまとまりです。 他のプログラミング言語では、これは無名関数やラムダと呼ばれます。 メソッドは、ブロックをProcに変換できます。Procはコードのまとまりを表すものです。

def my_method(&block)
  1 + yield
end

my_method { 2 }
# => 3

Crystalでは、繰り返し処理やコールバックなどで、ブロックとProcが非常によく使われます。 また、DSL(ドメイン固有言語)を作るのにも使われます。DSLの例としては、WebフレームワークのKemalや、テストフレームワークのSpec(Crystalの解答をテストするのに使われているものです)が挙げられます。

ブロック

ブロックを受け取れるメソッドを作るには、最後の引数の前にアンパサンド(&)を付ける必要があります。 こうすると、ブロックがProcに変換され、その引数に代入されます。 そしてyieldを使うと、メソッドに渡されたブロックが実行されます。

def my_method(&block)
  1 + yield
end

ブロックを渡す方法は2つあります。do ... endキーワードを使うか、波括弧({})を使うかです。 この2つの主な違いは優先順位です。do ... endは複数行のブロックをサポートしますが、波括弧はそうではありません。

my_method do
  2
end
# => 3

my_method { 2 }
# => 3

yield

yieldは、メソッドに渡されたブロックを実行するキーワードです。 つまり、yieldが呼び出されなければ、ブロックが実行されることはありません。 yieldには引数を渡すこともでき、その引数はブロックに渡されます。 ブロックに渡せる引数の数に制限はありません。 ブロック内でその引数を使うには、|x, y, ...|という構文を使います。ここで、x、y、...は引数の名前です。

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

上の例では、ブロックに引数2が渡され、ブロックはそれを3倍して6を返します。 次に、メソッドがその結果に1を足して7を返します。

yieldに渡す引数の数が、ブロックが期待する引数の数より多いと、エラーが発生します。

def my_method(&block)
  1 + yield
end

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

ブロックの型

ブロックは、明示的に書くことも暗黙的に書くこともできます。 暗黙的なブロックが最も一般的で、上の例で示したのはこちらです。 ブロックに異なる型の引数を与えることもでき、コンパイラーが型を推論してユニオン型を作ります。

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

my_method do |x|
  p typeof(x)
end

# output: (String | Int32)

明示的なブロックを書くときは、引数の型と戻り値の型を指定できます。 これはArg1Type, Arg2Type, ... -> ResultTypeという構文で行います。引数はカンマ(,)で区切り、結果の型は->で区切ります。

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

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

他の明示的な型と同様、コンパイラーは型が期待どおりであることを強制します。 ブロックがメソッドに指定した型と一致しない場合は、エラーが発生します。

Proc

Procはブロックと非常に深い関係があります。 決定的な違いは、Procは型であり、ブロックは型ではないという点です。 Procは関数ポインターを表し、変数として受け渡せます。

Procを定義するには、procリテラルの->(args) { ... }という構文を使います。 引数はカンマ(,)で区切り、ブロックは{ ... }で区切ります。 Procを呼び出すには、callメソッドを使い、そこに引数を渡します。 仮引数の型は必須です。 戻り値の型はprocの本体から推論されますが、明示的に指定することもできます。

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

ブロックのキャプチャ

ブロックのキャプチャは、ブロックからProcを作る方法です。 これは、メソッドの最後の引数の前に&演算子を使うことで行います。 こうすると、ブロックがProcに変換され、その引数に代入されます。

def my_method(&block)
  block.call
end

my_method { 1 + 2 }
# => 3

メソッドにProcを渡す

Procは、ブロックを期待するメソッドに渡せます。 条件に応じて異なるブロックを渡したいときに便利です。

def my_method(&block)
  yield 5
end

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

変数の型を宣言する

変数を宣言するときは、: Typeという構文でその変数の型を宣言できます。 procの場合は、Proc((ArgType, ...), ResultType)という構文を使います。 最後の型は常に戻り値の型で、残りは引数の型です。 つまり、型が1つだけならそれが戻り値の型となり、procは引数を取りません。

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

説明

記事やブログ記事、その他の文章コンテンツを掲載するウェブサイト向けの、コンテンツ管理システムを開発しているとします。 ここでは、優先度付きキューに入ったタスクを処理するタスクハンドラーの実装を任されています。 タスクハンドラーはタスクを受け取り、サーバーの負荷とタスクの優先度に基づいて、次にどのタスクを実行するかを決めます。 サーバーの負荷が高いときは、タスクを実行するために、より高い優先度の数値が必要になります。 サーバーは、負荷に応じて負荷を動的に変化させます。

1. TaskHandlerを初期化する

TaskHandlerは、キューとデフォルトのload_levelを指定して初期化する必要があります。

TaskHandlerクラスの初期化メソッドを実装します。このメソッドは、タスク名を表すStringのArrayを引数に取り、それを@tasksというインスタンス変数に格納します。 初期化メソッドでは、@task_condition_logicというインスタンス変数も作成します。この変数には、Int32を引数に取り、その値が0以上ならtrue、0より小さければfalseを返すProcを格納します。

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

2. タスクの条件ロジックを更新する

サーバーは、サーバーの負荷に応じてタスクの条件ロジックを更新できる必要があります。

ブロックを引数に取るTaskHandler#update_task_condition_logicメソッドを実装します。ブロックは@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. 次のタスクを実行する

サーバーは、キューにある次のタスクを実行できる必要があります。

@tasks配列の先頭のタスクが、次に実行するタスクです。 タスクは、与えられた優先度レベルを@task_condition_logicに渡した結果がtrueを返す場合に実行します。

タスクを実行できたかどうかに応じて、メッセージを返します。 タスクを実行できた場合は、"Completed <task>"を返します。 タスクを実行できなかった場合は、"Could not complete <task>"を返します。

タスクを実行できた場合は、@tasks配列からそのタスクを削除します。

次のタスクの優先度レベルをInt32として引数に取り、タスクを実行できたかどうかに応じてStringを返すTaskHandler#execute_next_taskメソッドを実装します。

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"
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Crystal Exercism

タスクハンドラーを始める準備はできましたか?

Exercismに登録すれば、26個のコンセプト142個の演習、そして本物の人間によるメンタリングとともに、Crystalを学んでマスターできます。すべて無料です。