Полное руководство по одной из самых мощных функций Ruby

Блоки являются одними из наиболее часто используемых функций в Ruby, и большинство разработчиков Ruby блаженно не подозревают об их наличии, в том числе о том, насколько мощными они могут быть. Блоки далеко не сложные, но и не совсем детские.

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

Всем, кто занимался программированием на Ruby, это должно показаться очень знакомым. do...end есть дом почти в каждом скрипте Ruby, который я когда-либо писал. Хотя технически стиль Ruby заключается в использовании {}, если блок можно сжать до одной строки:

5.times { puts “Hello Medium" }

За кулисами puts “Hello World” является блоком и вводится как анонимный параметр функции times. Если мы заглянем в документацию по API, то она нам и скажет! Эти документы также информируют нас, что наш блок может принимать аргумент, если мы того пожелаем:

Что выведет на консоль следующее:

Hello Medium 0
Hello Medium 1
Hello Medium 2
Hello Medium 3
Hello Medium 4

Другой распространенный вариант использования блоков - во время файлового ввода-вывода. Знали вы об этом или нет, каждый раз, когда вы открывали файл с помощью IO.foreach, вы фактически передавали блок, который выполнялся для каждой строки вашего файла!

Мы все время использовали блоки и почти не осознавали их огромный потенциал - давайте это изменим.

Блоки часто называют «анонимными методами», поскольку они могут быть неявно переданы в любой метод в Ruby. Все, что требуется для выполнения - или вызова - блока, - это yield к нему:

Здесь мы передаем нашему методу say_with_time блок, содержащий одну строку: «Hello Medium». Затем этот метод yields в свой блок и распечатывает текущее время вместе с ним.

Тем не менее, у этого метода есть странное поведение ... Если мы проверим его с помощью pry:

Первое, что мы заметим, это то, что наш блок не указан в качестве параметра в области видимости. Мы можем уступить ему и проверить его логический статус с помощью block_given?, но мы не можем вызвать его как параметр.

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

block теперь является локальной переменной в области видимости, и после ее проверки мы наконец выясняем происхождение нашего дорогого блока, The Object: Proc.

Proc

Процедуры, сокращенно от "Процедуры", по сути, представляют собой блоки, инкапсулированные внутри объекта Ruby. Procs предоставляют нашим блокам дополнительную функциональность и позволяют передавать их как обычные объекты.

Наивно, мы можем использовать Procs так же, как раньше использовали блоки:

Здесь мы явно определяем блок и передаем его в наш say_with_time метод, по сравнению с нашим первым примером, где блок был передан неявно. Преимущества здесь такие же, как и при сохранении примитивов в переменных: повторное использование. Вы не стали бы жестко кодировать «Hello Medium» везде, где захотите, вы бы сохранили его как переменную и использовали бы вместо этого, та же логика может быть применена к Procs / блокам!

Procs имеют гораздо больший потенциал, чем этот простой пример. Пределы того, что может выполнить Proc, ограничены только вашим воображением и творчеством. Как я уже упоминал, вы уступаете блоку, «вызывая» его. В этом смысле мы, по сути, только что представили функции обратного вызова в Ruby!

В метод может быть передано не более одного блока, прямо или косвенно, однако при желании вы можете отправить несколько процедур в метод:

Лямбды

Термин лямбда-выражения является синонимом анонимных функций в программировании и происходит от одноименной модели исчисления. Его техническое определение: Функция, определенная и, возможно, вызываемая без привязки к идентификатору ». Хотя блоки и процедуры также подходят под это определение, лямбды имеют несколько ключевых отличий, которые позволяют им выделяться среди толпы.

Во-первых, давайте исследуем синтаксис лямбда в отличие от синтаксиса блоков и процедур:

У лямбда-выражений есть несколько ключевых отличий от процедур, которые могут быть весьма полезными в зависимости от того, как вы на них смотрите.

  • Лямбды определяют количество аргументов, которые вы им передаете.
  • Лямбды обрабатывают ключевое слово return иначе, чем Procs

Лямбда-функции выдадут ArgumentError, если вы передадите неправильное количество аргументов, тогда как Procs просто заполнит пробелы с помощью nil.

Для процедур return означает «возврат из окружающего метода», тогда как для лямбда-выражений return означает «возврат из лямбда-выражения».

Вот результат выполнения вышеуказанного скрипта:

» ruby test_proc_lambda_returns.rb
About to call Proc
in the proc!
Running Lambda
in the lambda
Done Running Lambda

Обратите внимание на «Готово, Proc!» в строке 8 никогда не запускается, так как return внутри нашего proc_return выпрыгивает из всего test_proc_returns метода.

Передача и выполнение блоков

Поскольку мы почти превратились в экспертов по блокам, пришло время применить наши вновь приобретенные навыки. Мы исследовали неявную передачу блоков в методы Ruby, теперь пришло время сделать этот пост явным. Используя мощный символ & перед переменной в заголовке нашего метода, мы можем преобразовать любой Proc / Lambda в блок!

В этом примере мы начали подробно рассматривать варианты использования блоков как функций обратного вызова. print_each_value - это, по сути, метод, инкапсулированный в лямбда, который я выбрал вместо Proc, потому что мы хотим убедиться, что у него есть аргумент для печати. Не меняя print_blocks метод, мы можем вместо этого создать новые лямбда-выражения, чтобы изменить его функциональность. Приведенный выше фрагмент выводит следующее:

» ruby print_blocks.rb
lets print each value verbatim
0
1
2
3
4
5
6
7
8
Now let's double down
0
2
4
6
8
10
12
14
16

Этот пример все еще наивен, однако теперь, когда мы понимаем &, мы можем полностью погрузиться в блочные обратные вызовы.

Как я сказал ранее, возможности здесь только самоограничиваются. Мантра Руби никогда не заключалась в том, чтобы спрашивать «почему?», Функциональность предоставляется вам, и вам решать, что с ее помощью создавать. Вот несколько интересных примеров, которые могут вас заинтересовать:

Пытаться

Метод, который принимает логическое значение, массив переменных аргументов и блок. Попытка затем выполняет блок, передавая аргумент [0], и проверяет, если возникает ошибка.

С нашей первой попытки попытка уже доказала свою ценность:

После исправления этой ошибки мы можем повторно запустить наш скрипт:

» ruby try.rb
Hello World
HELLO WORLD
hello world

Мне нравится этот пример, потому что мое дерьмо часто ломается, и возможность сразу определить, почему он не работает, и увидеть объем указанного сбоя, позволяет мне решать свои проблемы намного быстрее. Этот пример легко расширить, поскольку *params может содержать любое количество аргументов - просто помните, что наш блок ВСЕГДА должен находиться в конце списка параметров.

Выполнить вокруг

Передача блоков в методы достаточно распространена в Ruby, поэтому у нее есть собственное имя: Execute Around (блок). В этой модели программирования мы определяем метод, который принимает блок и выполняет ряд задач, связанных с ним, таких как печать, ведение журнала, доступ к базе данных или все, что угодно вашему маленькому программисту.

» ruby execute_around.rb
86400
1
-1

И вывод журнала из log_1556468836.txt:

Starting 'Calculate seconds in a day'
Completed 'Calculate seconds in a day'
Starting 'Open a file I know doesn't exist'
'Open a file I know doesn't exist' FAILED!

Быстрый обзор

  • Блоки - это анонимные функции, неявно переданные в функции. Это экземпляры класса Proc, которые принимают свою область видимости при передаче в метод.
  • Procs бывают двух форм: Proc и Lambda, которые различаются только принудительным применением аргументов и обработкой ключевого слова return.
  • Процедуры, лямбды и блоки - все это примеры closures в Ruby
  • Procs / Lambdas могут быть преобразованы в блоки с символом &
  • Возможности блоков проявляются, когда они реализованы в виде функций обратного вызова.