Блок - це фрагмент коду, який можна передати методу. В інших мовах програмування це називають анонімною функцією або лямбдою. Метод може перетворити блок на 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.
Йому також можна передати аргументи, які отримає блок.
Кількість аргументів, які можна передати блоку, не обмежена.
Щоб скористатися аргументами в блоці, застосовують синтаксис |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 ->(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 можна передавати методам, які приймають блок. Це зручно, коли потрібно передавати різні блоки залежно від певної умови.
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 }
Ми розробляємо систему керування вмістом для вебсайту, де публікуються статті, дописи в блогах та інший письмовий вміст. Наше завдання - реалізувати обробник завдань, який опрацьовуватиме завдання в черзі з пріоритетами. Обробник завдань отримуватиме завдання і на основі навантаження на сервер та їхнього пріоритету вирішуватиме, яке завдання виконувати наступним. Якщо сервер зазнає великого навантаження, тоді число пріоритету має бути вищим, щоб завдання було виконано. Сервер динамічно змінюватиме навантаження залежно від навантаження.
TaskHandler
TaskHandler потрібно ініціалізувати з чергою та типовим значенням load_level.
Реалізуйте ініціалізатор класу TaskHandler, який приймає як аргумент Array з String-ів із назвами завдань і зберігає його у змінній екземпляра @tasks.
Ініціалізатор також має створити змінну екземпляра @task_condition_logic, яка зберігає Proc, що приймає Int32 і повертає true, якщо аргумент дорівнює 0 або більший за 0, та false, якщо він менший за 0.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
# => #<TaskHandler:0x7fd8d4cfbf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562536a4a330>>
Сервер має вміти оновлювати логіку умови завдання залежно від навантаження на сервер.
Реалізуйте метод 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>>
Сервер має вміти виконувати наступне завдання в черзі.
Перше завдання в масиві @tasks буде виконано наступним.
Завдання потрібно виконати, якщо заданий рівень пріоритету завдання дає true, коли його передати в Proc @task_condition_logic.
Метод повертає повідомлення залежно від того, чи було завдання виконано.
Якщо завдання було виконано, метод має повернути: "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"
Зареєструйтеся на Exercism, щоб вивчати й опановувати Crystal, а також 26 концепцій142 вправи та справжнє наставництво від людей, і все це безкоштовно.