學習軌道
/
Crystal
Crystal
/
練習
/
任務處理器
任務處理器

任務處理器

學習練習

簡介

區塊是一段可以傳遞給方法的程式碼。 在其他程式語言中,這被稱為匿名函式或 lambda。 方法可以將區塊轉換成 Proc,它代表一段程式碼。

def my_method(&block)
  1 + yield
end

my_method { 2 }
# => 3

區塊和 Proc 在 Crystal 中非常常用,例如用於疊代和回呼。 不過也能用來建立 DSL(領域特定語言),DSL 的例子包括網頁框架 Kemal 或測試框架 Spec(這正是用來測試你的 Crystal 解答的工具)。

區塊

若要建立可接受區塊的方法,你需要在最後一個引數前加上 & 符號。 這會將區塊轉換成 Proc 並指定給該引數。 接著使用 yield 就會執行傳給該方法的區塊。

def my_method(&block)
  1 + yield
end

傳遞區塊有兩種方式:使用 do ... end 關鍵字,或使用大括號({})。 兩者的主要差異在於優先順序:do ... end 支援多行區塊,而大括號({})則不支援。

my_method do
  2
end
# => 3

my_method { 2 }
# => 3

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) 語法。 最後一個型別一定是回傳型別,其餘的則是引數型別。 也就是說,如果只有一個型別,它就是回傳型別,而該 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,用來存放一個 Proc,這個 Proc 接受一個 Int32,並在引數等於或大於 0 時回傳 true,小於 0 時回傳 false。

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 proc 後回傳 true,就應該執行該任務。

它會根據任務是否被執行回傳對應的訊息。 如果任務被執行,應該回傳:"Completed <task>"。 如果任務沒有被執行,應該回傳:"Could not complete <task>"。

如果任務被執行,就應該從 @tasks 陣列中移除。

實作 TaskHandler#execute_next_task 方法,讓它接受下一個任務的優先級 Int32 作為引數,並根據任務是否被執行回傳 String。

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,全部免費。