Блоки в 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
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1# frozen_string_literal: true
2
3require 'test_helper'
4require_relative 'index'
5
6describe 'function' do
7 it 'should works 1' do
8 output = /3\n2\n1/
9 assert_output(output) do
10 show_me_numbers(3)
11 end
12 end
13
14 it 'should works 1' do
15 output = /4\n3\n2\n1/
16 assert_output(output) do
17 show_me_numbers(4)
18 end
19 end
20end
21
Решение учителя откроется через: