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. Это важно, когда идет вложенный вызов нескольких функций, и каждая из них умеет работать с блоками:

 # в обоих примерах функции f1 и f2 вызываются
 # Подставьте в них мысленно скобки

 # здесь блок будет относиться к функции f1
 f1 f2 do # f1(f2() do ... end)
 end

 # здесь блок будет относиться к функции f2
 f1 f2 {} # f1(f2()) {}

Не переживайте, если прямо сейчас блоки вам непонятны. Для их осознания нужно время и практика. В Ruby они повсеместно, поэтому понимание работы с блоками приходит быстро. Буквально в следующем модуле они будут уже везде.

Задание

Реализуйте функцию, которая выводит числа от нуля до переданного в функцию в обратном порядке:

show_me_numbers(3)
# 3
# 2
# 1

Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе
Упражнение доступно только авторизованным пользователям.

Пожалуйста, авторизуйтесь, это необходимо для отслеживания прогресса выполнения уроков. Если у вас ещё нет учётной записи, то сейчас самое время создать аккаунт.