Ruby: Блоки
Блоки в Ruby — очень важная концепция, которая встречается на каждом шагу. У неё нет аналогов в популярных языках, поэтому при изучении блоков сложно опираться на прошлый опыт. К счастью, их не так сложно понять, особенно если у вас есть опыт работы с лямбда-функциями (анонимными функциями). Начнём с примера:
# Переводится как «пять раз». В этом весь Ruby.
5.times do |i|
puts i
end
# => 0
# => 1
# => 2
# => 3
# => 4
Всего три строчки, но очень много новых смыслов. Если говорить в общем, то здесь вызывается метод times()
, который принимает на вход блок кода и вызывает его пять раз.
Блок кода — это конструкция do end. Блок очень похож на функцию, которая передается в функцию times()
. Но передается довольно необычным способом. Ключевое слово do начинается после того, как закрыты вызывающие скобки у метода. Блок просто отделяется пробелом от вызова самой функции.
# После пятёрки нет запятой!
5.times() do |i|
puts i
end
# А так не сработает
5.times(do |i|
puts i
end)
Как это работает? Блоки в Ruby обычно передаются в функции, как особый аргумент, который идёт вне вызова функции, что видно по примеру сверху. Внутреннюю работу блоков в функциях мы рассмотрим позже, когда немного научимся использовать блоки.
Это довольно необычная концепция. Сама по себе она не привносит никаких новых возможностей в язык, но даёт новые визуальные возможности по оформлению кода. Именно из-за этой особенности Ruby так хорошо подходит и часто используется, как язык для построения DSL (языков предметной области). Подробнее об этом в следующих уроках.
И, наконец, сам блок. Можно представить, что внутри функции он попадает в переменную, которая вызывается, как обычная функция. Сам блок — как функция (а он является в том числе функцией), и умеет принимать параметры. Внутрь блока они попадают через конструкцию |i|
, идущую сразу после do. Этот синтаксис пришел в Ruby из Smalltalk. Если параметров несколько, то они просто перечисляются через запятую |one, two|
.
Блок работает как замыкание, а значит внутри него можно использовать любые переменные, определенные снаружи и выше блока:
name = 'ruby'
3.times do # а параметры блока можно опускать
puts name
end
# => ruby
# => ruby
# => ruby
У блоков есть альтернативный синтаксис. Пример выше можно было записать так:
5.times { |i| puts i }
Подобную запись используют в том случае, когда содержимое блока помещается на одну строку. Синтаксис do/end
никогда не используют для однострочных блоков.
Если быть до конца честными, то эти два синтаксиса работают немного по-разному. У {}
приоритет выше, чем у do/end
. Это важно, когда идёт вложенный вызов нескольких функций, и каждая из них умеет работать с блоками. Давайте разберём пример:
# Обе функции вызываются и печатают на экран приветствие, если им передан блок
# Подставьте в них мысленно скобки
print_hello_f1 { "Dima" }
# => "Hello from f1, Dima"
print_hello_f2 { "Vasya" }
# => "Hello from f2, Vasya"
# При таком вызове функции ничего не печатают, так как нет блока
print_hello_f1
print_hello_f2
print_hello_f1 print_hello_f2 { "Dima" }
# => ?
print_hello_f1 print_hello_f2 do
"Vasya"
end
# => ?
Однострочный вариант блока будет относиться к самой правой функции. При полной форме do ... end
блок относится к самой первой функции
print_hello_f1 print_hello_f2 print_hello_f3 { 'Petya' }
# => "Hello from f3, Petya"
print_hello_f1 print_hello_f2 print_hello_f3 do 'Vasya' end
# => "Hello from f1, Vasya"
С помощью скобок можно определить, к какой функции блок будет относиться:
# Равнозначные варианты. Скобки определяют, куда будет отнесён блок
# f1(f2()) do ... end
print_hello_f1(print_hello_f2()) { 'Petya' }
# => "Hello from f1, Petya"
print_hello_f1 print_hello_f2 do
'Petya'
end
# => "Hello from f1, Petya"
# Равнозначные варианты
# f1(f2() do ... end)
print_hello_f1(print_hello_f2() { 'Petya' })
# => "Hello from f2, Petya"
print_hello_f1(print_hello_f2 do
'Petya'
end)
# => "Hello from f2, Petya"
Не переживайте, если прямо сейчас блоки вам непонятны. Для их осознания нужно время и практика. В Ruby они встречаются повсеместно, поэтому понимание работы с блоками приходит быстро. Буквально в следующем модуле они будут уже везде.
Задание
Реализуйте функцию show_me_numbers()
, которая выводит на экран числа от одного до переданного в функцию в обратном порядке:
show_me_numbers(3)
# => 3
# => 2
# => 1
Упражнение не проходит проверку — что делать? 😶
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя 🤔
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно 🙄
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.