Skip to content

Latest commit

 

History

History
728 lines (489 loc) · 54.4 KB

ruby-peredacha-v-metod-bloka-proc-i-lambda.md

File metadata and controls

728 lines (489 loc) · 54.4 KB

Ruby: Передача в метод блока, proc и lambda

  1. Передаем методу вызывающему блок proc и lambd-у вместо блока

  2. Разбираемся, как работает преобразование proc-а и lambd-ы в блок

  3. Передаем методу proc-и и лямбды в качестве обычных аргументов

  4. Преобразуем блок переданный методу в proc-объект

  5. Когда нужно преобразовывать блок переданный методу в proc-объект

  6. Создание proc-а с помощью метода Symbol#to_proc

  7. Разбираемся, как устроен метод Symbol#to_proc

  8. Заключение

#1. Передаем методу вызывающему блок proc и lambd-у вместо блока

Пусть мы имеем метод, который вызывает блок переданный ему при вызове:

def met
  yield
end

При вызове метода met он сразу же вызывает переданный ему блок и возвращает то, что вернул этот блок.

Вызовем этот метод и передадим ему блок, который просто возвращает строку "block":

met { 'block' }  # => "block"

Как видим, метод met возвращает то, что получил от блока — строку "block".

Теперь определим одну лямбду и один proc:

lam = lambda { 'lam' }
pr  = proc { 'pr' }

Попытаемся вызвать метод met и передать ему поочередно эти лямбду и proc:

met lam  # => `met': wrong number of arguments (given 1, expected 0) (ArgumentError)
met pr   # => `met': wrong number of arguments (given 1, expected 0) (ArgumentError)

met(lam)  # => `met': wrong number of arguments (given 1, expected 0) (ArgumentError)
met(pr)   # => `met': wrong number of arguments (given 1, expected 0) (ArgumentError)

Как видим, наш метод отказывается воспринимать лямбду и proc вместо блока. Он считает их обычными аргументами. А т.к. он объявлен как метод без аргументов (def met), то поэтому он и выбрасывает соответствующую ошибку.

Нам нужен какой-то способ конвертировать лямбду и proc в блок.

И в Ruby есть такой способ — унарный &-оператор. Ставя этот оператор перед лямбдой или proc-ом при вызове метода, мы сообщаем Ruby о своем желании преобразовать их в блок и передать его вызываемому методу. Есть только одно ограничение — если метод принимает какие-либо аргументы, то при вызове метода &-аргумент должен идти последним аргументом. Но в нашем случае, когда метод вообще не принимает никаких аргументов, мы можем не заботиться об этом.

Давайте посмотрим, как это работает:

met(&lam)  # => "lam"
met(&pr)   # => "pr"

Как видим трюк с преобразованием лямбды и proc-а с помощью &-оператора сработал. Ruby видит, что мы вызываем метод met и начинает вычислять аргументы передаваемые методу при его вызове. Встречая в списке аргументов аргумент с префиксом в виде символа &, Ruby преобразует его в блок. И только после этого он переходит к выполнению тела самого метода. А тело нашего метода met очень простое — вызов блока. После того как выполняется тело блока метод возвращает то, что он получил от этого блока — в нашем случае это просто обычные строки, соответственно "lam" и "pr". Метод обращается с блоком преобразованным с помощью &-оператора из proc-а или лямбды, так же как и с обычным блоком.

#2. Разбираемся, как работает преобразование proc-а и lambd-ы в блок

Обратимся к книге "The Ruby Programming Language" написанной создателем языка Ruby Юкихиро Матцумото в соавторстве со своим другом Дэвидом Фланагеном. В разделе 6.4.5.1 Using & in method invocation читаем:

In a method invocation an & typically appears before a Proc object. But it is actually allowed before any object with a to_proc method.

При вызове метода символ & обычно встречается перед объектом класса Proc. Но в действительности разрешается, чтобы он находился и перед любым другим объектом, который отвечает на метод to_proc.

Посмотрим, как Ruby преобразует в блок объект нашего кастомного класса:

class Klass
  def to_proc
  end
end

В соответствии с книгой мы определяем в нашем классе метод с именем to_proc. Хотя в книге и не говорится, что должен возвращать этот метод, но судя по его имени, он должен возвращать экземпляр класса Proc. Пусть этот proc возвращает строку, по которой мы сможем однозначно определить, какой proc-объект был вызван:

class Klass
  def to_proc
    proc { 'Klass#to_proc' }
  end
end

На текущий момент вот как выглядит весь наш код:

def met
  yield
end

lam = lambda { 'lam' }
pr  = proc { 'pr' }

class Klass
  def to_proc
    proc { 'Klass#to_proc' }
  end
end

Теперь все готово для проведения эксперимента. Создадим объект нашего класса и вызовем метод met, передавая ему этот объект, не забыв предварить его &-оператором:

obj = Klass.new

met(&obj)  # => "Klass#to_proc"

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

Ещё раз пройдемся по тому, что здесь произошло. Мы вызываем метод и передаем ему объект нашего класса с префиксом &. Ruby видит, что он должен вызвать метод met, и он начинает вычислять аргументы переданные методу. Встречая среди аргументов аргумент с префиксом &, он понимает, что ему нужно конвертировать его в блок. Для этого он проверяет класс этого &-аргумента. Если это класс Proc, то он просто преобразует этот proc в блок, а если это объект какого-то кастомного класса, то Ruby вызывает на этом объекте метод to_proc, а уже затем полученный proc-объект преобразует в блок. Только теперь Ruby готов вызвать наш метод met, что он и делает. Метод же met просто вызывает переданный ему блок. А блок, в свою очередь, возвращает строку "Klass#to_proc". Снова вступает в работу метод met — он возвращает наружу то, что получил от блока — строку "Klass#to_proc".

Давайте пронаблюдаем всю эту последовательность вызовов и возвратов с помощью трассировщика идущего в комплекте с Ruby — TracePoint.

def met
  yield
end

lam = lambda { 'lam' }
pr  = proc { 'pr' }

class Klass
  def to_proc
    proc { 'Klass#to_proc' }
  end
end

obj = Klass.new

# Настраиваем трассировщик
trace = TracePoint.new(:call, :return, :c_call, :c_return, :b_call, :b_return) do |tp|
  p [tp.lineno, tp.defined_class, tp.method_id, tp.event]
end

trace.enable  # Включаем отслеживание

met(&obj)

Наблюдаем вывод информации в консоли:

[9, Klass, :to_proc, :call]       # Вызывается метод Klass#to_proc
[10, Kernel, :proc, :c_call]      # Создается proc-объект proc { 'Klass#to_proc' }
[10, Kernel, :proc, :c_return]    # Завершается создание proc-объекта proc { 'Klass#to_proc' }
[11, Klass, :to_proc, :return]    # Выход из метода Klass#to_proc
[1, Object, :met, :call]          # Передача управления нашему методу met
[10, Klass, :to_proc, :b_call]    # Передача управления блоку
[10, Klass, :to_proc, :b_return]  # Выход из блока
[3, Object, :met, :return]        # Выход из метода met

Теперь пронаблюдаем за ситуацией при вызове метода met с передачей ему лямбды вместо блока:

trace.enable  # Включаем отслеживание

met(&lam)

[1, Object, :met, :call]          # Передача управления нашему методу met
[5, nil, nil, :b_call]            # Передача управления блоку, преобразованному из лямбды
[5, nil, nil, :b_return]          # Выход из блока
[3, Object, :met, :return]        # Выход из метода met

Аналогично для proc-а:

trace.enable  # Включаем отслеживание

met(&pr)

[1, Object, :met, :call]          # Передача управления нашему методу met
[6, nil, nil, :b_call]            # Передача управления блоку, преобразованному из proc-а
[6, nil, nil, :b_return]          # Выход из блока
[3, Object, :met, :return]        # Выход из метода met

Как видим, если при вызове метода унарному &-оператору передавать лямбду или proc, то он просто преобразует их в блок. Если же присмотреться более внимательно, то можно заметить, что фактически никакого преобразования не происходит — используется тот же самый блок, который был создан при определении лямбды или proc-а — в нашем листинге они определяются на строках 5 и 6 соответственно. Говоря простыми словами — при вызове метода &-оператор как бы отбрасывает "слова" lambda и proc, т.е. met(&lambda { 'lam' }) плавно превращается в met { 'lam' }. По завершению "преобразования" происходит вызов метода с передачей ему этого блока. В случае же когда мы передаем &-оператору объект какого-либо кастомного класса, Ruby сначала вызывает на этом объекте метод to_proc, а затем "преобразует" полученный proc в блок и только после этого передает управление самому методу.

#3. Передаем методу proc-и и лямбды в качестве обычных аргументов

Как известно всё (на самом деле почти всё) в Ruby является объектами. Не являются исключением и лямбды с proc-ами, которые представляют собой экземпляры класса Proc. Чтобы передать методу при вызове какой-либо объект, необходимо при объявлении этого метода с помощью ключевого слова def задать соответствующий параметр.

Рассмотрим метод принимающий один параметр:

def met_with_arg(x)
  # Тело метода
end

Пусть этот метод умеет работать только с proc-ами и лямбдами:

def met_with_arg(prc)
  # Тело метода
end

Что такой метод может сделать с полученным proc-ом или лямбдой?

  • Он может просто его/её проигнорировать (просто не обращаться к своему аргументу):
def met_with_arg(prc)
  # Метод делает свою работу, никак не обращаясь к prc
end
  • Он может его/её вызвать:
def met_with_arg(prc)
  prc.call    # Без передачи аргументов
  prc.()

  prc.call(0) # или с передачей каких-либо аргументов
  prc.(0)
end
  • Он может вызвать какой-то другой метод и передать этот Proc-объект вызываемому методу в качестве обычного аргумента (так же как сам был вызван):
def met_with_arg(prc)
  # Здесь метод делает свою работу (опционально)

  # А вот здесь метод вызывает другой метод
  another_met(prc)

  # И здесь метод тоже что-то делает (опционально)
end

def another_met(prc)
  # Тело метода
end
  • Он может вызвать другой метод преобразовав Proc-объект в блок:
def met_with_arg(prc)
  # Здесь метод делает свою работу (опционально)

  # Метод вызывает другой метод передавая ему блок, преобразованный из proc-а или лямбды
  another_met(&prc)

  # И здесь метод тоже что-то делает (опционально)
end

def another_met
  # Тело метода
end
  • И наконец, метод может быть какой-либо комбинацией вышеприведенных паттернов.

Как видите, есть несколько вариантов того, что метод может сделать с proc-ом и лямбдой, который он принял при вызове в качестве своего аргумента. А вот какой из этих вариантов использовать зависит от конкретной задачи. Однако, стоит признать, что задача при решении которой оказывается недостаточно передать методу блок и требуется дополнительно передавать лямбду является очень редкой. Мы же её здесь рассмотрели для полноты картины и чтобы показать, что такой вариант тоже возможен.

#4. Преобразуем блок переданный методу в proc-объект

Метод может преобразовать блок, переданный ему при вызове в объект класса Proc, если при объявлении этого метода последним идет аргумент с префиксом в виде символа &. Вот как это выглядит:

def met(&prc)

Тело метода

end

Вызов такого метода с блоком может выглядеть следующим образом:

met { 'Some string' } met { |x| x } met { |x, y| x + y }

Т.е. ничего нового — обычный вызов метода с блоком. Если же вызываемый метод не вызывает блок с помощью yield и не вызывает на своем аргументе prc метод call, то можно вообще не передавать никакого блока при вызове такого метода:

met

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

Когда Ruby видит, что он должен вызвать метод, то сначала он начинает вычислять аргументы переданные методу. Затем он парсит (считывает и анализирует) блок, который передается методу. Блок не является аргументом, т.к. он не вычисляется при вызове метода, а только парсится. В нашем случае мы не передаем методу никаких аргументов, но зато передаем блок. Далее Ruby находит определение метода (то место в программе, где мы определили метод с помощью def) и создает в памяти т.н. контекст (ещё говорят "область видимости" или "скоуп" — он англ. "scope"). Потом Ruby начинает наполнять этот скоуп, т.е. для каждого параметра метода он создает локальную переменную с таким же именем и присваивает этой переменной соответствующее значение аргумента, которое он вычислил, заметив вызов этого метода. Дойдя до параметра, начинающегося с символа & (в нашем случае — &prc) Ruby создает локальную переменную prc, преобразует блок переданный методу в Proc-объект и присваивает его переменной prc. И только сделав все это, Ruby начинает выполнять тело самого метода.

Теперь давайте посмотрим, что происходит при выполнении тела вызванного метода. Взглянем на конкретный код:

def met(&prc)

Тело метода

end

met { 'Some string' }

Мы вызываем метод и передаем ему блок.

Обратите внимание, что в теле метода мы имеем доступ к двум сущностям:

к блоку (т.е. при преобразовании в Proc-объект блок никуда не исчезает)

    мы можем вызвать его с помощью ключевого слова yield с аргументами или без

    def met(&prc)
      yield
      yield 1, 2
    end

к Proc-объекту полученному преобразованием блока

    мы можем его вызвать с аргументами или без

    def met(&prc)
      prc.call
      prc.call(1, 2)
      prc.(1, 2)
    end

    мы можем вызвать другой метод и передать ему этот Proc-объект в качестве обычного аргумента или в качестве блока

    def met(&prc)
      another_met(prc)   # Передаем как обычный аргумент
      another_met(&prc)  # Передаем как блок
    end

Как видите мы можем преобразовать блок переданный методу в proc-объект. Причем при таком преобразовании блок никуда не исчезает.

Давайте с помощью трассировщика заглянем на уровень ниже и посмотрим, как на самом деле происходит преобразование блока в proc.

def met(&blk) blk.call # Вызываем proc-объект полученный преобразованием из блока end

Настраиваем трассировщик

trace = TracePoint.new(:call, :return, :c_call, :c_return, :b_call, :b_return) do |tp| p [tp.lineno, tp.defined_class, tp.method_id, tp.event] end

trace.enable # Включаем отслеживание

met { 'blk' }

[1, Object, :met, :call] # Вход в метод met [11, nil, nil, :b_call] # Вызов блока определенного на строке 11 [11, nil, nil, :b_return] # Выход из блока [3, Object, :met, :return] # Выход из метода met

Обратите внимание, что хотя метод met обращаемся не к блоку, а к proc-объекту, в который преобразуется блок, в реальности вызывается именно блок, определенный на 11-й строке.

А вот что происходит с лямбдой, которую передают в метод в качестве блока, а в методе обратно преобразуют в proc-объект:

def met(&blk) blk.call # Вызываем proc-объект полученный преобразованием из блока end

lam = lambda { 'lam' }

Здесь идет код для настройки и запуска трекера, как и в предыдущем листинге

met(&lam) # Преобразуем лямбду в блок и вызываем метод met

[1, Object, :met, :call] # Вход в метод met [5, nil, nil, :b_call] # Вызов блока определенного на строке 5 (это блок лямбды) [5, nil, nil, :b_return] # Выход из блока [3, Object, :met, :return] # Выход из метода met

В реальности никакого двойного преобразования не происходит. Просто вызывается блок принадлежащий лямбде.

Посмотрим на "преобразования" proc-а:

def met(&blk) blk.call # Вызываем proc-объект полученный преобразованием из блока end

pr = proc { 'pr' }

Здесь идет код для настройки и запуска трекера, как и в предыдущем листинге

met(&pr) # Преобразуем proc в блок и вызываем метод met

[1, Object, :met, :call] # Вход в метод met [5, nil, nil, :b_call] # Вызов блока определенного на строке 5 (это блок принадлежащий proc-у с 5-й строки) [5, nil, nil, :b_return] # Выход из блока [3, Object, :met, :return] # Выход из метода met

И с proc-ом нет никакого двойного преобразования. Просто вызывается блок принадлежащий proc-у.

Концепции блока, proc-а и лямбды являются концепциями, определенными на уровне Ruby, т.е. высокоуровневыми. Однозначно можно сказать, что преобразования между блоками, proc-ами и лямбдами являются бесплатными — они выполняются практически мгновенно. А с практической точки зрения можно считать, что &-оператор то "отбрасывает" "слова" proc и lambda от блока, то "приклеивает" их обратно. #5. Когда нужно преобразовывать блок переданный методу в proc-объект

В языке программирования Ruby, где как известно всё является объектом, блок объектом не является!

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

С блоком мы можем сделать только две вещи:

передать методу при вызове (причем метод вовсе не обязан что-то делать с переданным ему блоком)

вызвать из метода с помощью ключевого слова yield

Когда мы не можем сделать с какой-то сущностью то, что мы можем сделать с другими, говорят, что эта сущность не являестя First-class citizen (не являются полноценной сущностью).

Давайте рассмотрим конкретную задачу — создать метод-обертку (wrapper method) для какого-то другого метода.

def met

Тело метода

end

def wrapper

Здесь враппер делает действия, ради которых он и создан. Например, записывает в лог

met

Здесь враппер тоже может что-то делать

end

Наш враппер прекрасно работает...

def met 'Hello!' end

def wrapper

Здесь враппер делает действия, ради которых он и создан. Например, он записывает в лог

met

Здесь враппер тоже может что-то делать (но это опционально)

end

Вместо met мы вызываем наш враппер

wrapper # => "Hello!"

Но только до тех пор пока тот метод вокруг которого мы обернули наш враппер не вызовет блок:

def met name = yield "Hello, #{name}!" end

def wrapper

Здесь враппер делает действия, ради которых он и создан. Например, он записывает в лог

met

Здесь враппер тоже может что-то делать (но это опционально)

end

wrapper # => `met': no block given (yield) (LocalJumpError)

Такая ошибка происходит тогда, когда вызывается метод вызывающий блок (метод met в нашем примере) без блока.

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

def met name = yield "Hello, #{name}!" end

def wrapper(&blk) # 1-е преобразование: Блок преобразуется в proc-объект

Здесь враппер делает какие-то действия ради которых он и создан. Например, он записывает в лог

met(&blk) # 2-е преобразование: proc преобразуется в блок. А потом вызывается метод met

Здесь враппер тоже может что-то делать (но это опционально)

end

Вызываем наш враппер и передаем ему блок

wrapper { 'John' } # => "Hello, John!"

Как видите, нашему блоку приходится нелегко. Его преобразовывают два раза. Это является следствием того, что блоки не являются First-class citizens (полноценными сущностями). В Ruby блоки не являются объектами. #6. Создание proc-а с помощью метода Symbol#to_proc

Давайте посмотрим на следующий код:

['a', 'b', 'c'].map { |letter| letter.upcase } # => ["A", "B", "C"] ['a', 'b', 'c'].map(&:upcase) # => ["A", "B", "C"]

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

Вторая строка во многом похожа на первую. Так же совпадают и результаты выполнения обеих строчек кода. Однако, есть и существенные различия:

первая строчка ощутимо длиннее;

в первой строчке методу map передается блок { |letter| letter.upcase } в то время как во второй ему передается некий странно выглядящий аргумент &:upcase.

Можно предположить, что вторая строчка просто является синтаксическим сахаром для первой. Т.е. праще говоря, что это некий трюк позволяющий писать более краткий код. Именно такое мнение и доминирует в интернете.

Давайте попробуем разобраться, действительно ли это какай-то трюк или всё же здесь присутствует определенная логика, которую мы пока не понимаем.

Взглянем ещё раз на строчку ['a', 'b', 'c'].map(&:upcase) и проговорим вслух то что там происходит:

мы вызываем на массиве ['a', 'b', 'c'] метод map с неким аргументом &:upcase

мы вызываем метод map c аргументом &:upcase

мы вызываем метод c аргументом &:upcase

мы вызываем некий метод с аргументом &:upcase

Стоп! Отбросим пока несущественные подробности и запишем ту фразу, к которой мы только что пришли (мы вызываем некий метод с аргументом &:upcase) на языке Ruby:

met(&:upcase)

Что-то похожее мы уже видели во 2-й части этой статьи, когда разбирались с тем как передать методу лямбду и proc вместо блока. Давайте вспомним, как мы это делали:

def met yield end

lam = lambda { 'lam' } pr = proc { 'pr' }

met(&lam) # => "lam" met(&pr) # => "pr"

Потом мы выяснили, что при вызове метода унарный оператор & преобразует лямбду и proc в блок. После этого мы пошли дальше и установили, что &-оператор может преобразовывать в блок не только лямбды и proc-и, но даже объекты кастомных классов имеющих метод to_proc.

Давайте сделаем предположение, что странно выглядящий аргумент &:upcase вызова метода — это на самом деле случай использования унарного &-оператора, которому передается символ :upcase (экземпляр класса Symbol). Обратившись к документации на класс Symbol мы можем заметить, что он имеет метод экземпляра to_proc. Здесь очень соблазнительно сделать поспешный вывод о том, что &-оператор вызывает на этом символе метод to_proc и преобразует полученный proc-объект в блок — так же как он это делает с экземплярами кастомных классов. Однако истина заключается в том, что класс Symbol не является ни кастомным ни даже классом стандартной библиотеки — он входит в ядро языка Ruby. А ядро Ruby написано на Си. Язык Си — это не объектно-ориентированный язык программирования — там нет ни объектов, ни методов. Даже если мы запустим трассировщик для того чтобы увидеть вызов метода Symbol#to_proc или создание блока, то мы этого там не увидим, т.к. объекты, методы, блоки, лямбды и proc-и — это более высокоуровневые сущности чем те с которыми имеет дело язык Си. Поэтому чтобы разобраться в том, что же такое &:symbol мы не должны покидать "королевство" Ruby.

Воспроизведем тот код с которого мы начали эту часть статьи в упрощенной форме:

def met yield 'a' end

met { |letter| letter.upcase } # => "A" met(&:upcase) # => "A"

Как мы видим наш метод met работает с сущностью созданной с помощью &:symbol так же как и с обычным блоком. А сама эта сущность возвращает такой же результат, как и обычный блок.

Давайте познакомимся с этой сущностью поближе. И для этого мы создадим специальный метод blk_to_prc, который нам поможет добыть эту сущность:

def blk_to_prc(&blk) blk end

prc_from_blk = blk_to_prc { |letter| letter.upcase } prc_from_sym = blk_to_prc(&:upcase)

У нашего вспомогательного метода blk_to_prc только одна задача — преобразовать переданный ему блок в Proc-объект и вернуть его наружу.

Давайте посмотрим, что добыл для нас blk_to_prc:

prc_from_blk = blk_to_prc { |letter| letter.upcase } prc_from_blk.class # => Proc prc_from_blk.lambda? # => false prc_from_blk.parameters # => [[:opt, :letter]] prc_from_blk.arity # => 1 prc_from_blk.() # =>

': undefined method `upcase' for nil:NilClass (NoMethodError) prc_from_blk.('a') # => "A" prc_from_blk.('a', 'b') # => "A"

prc_from_sym = blk_to_prc(&:upcase) prc_from_sym.class # => Proc prc_from_sym.lambda? # => false prc_from_sym.parameters # => [[:rest]] prc_from_sym.arity # => -1 prc_from_sym.() # => <main>': no receiver given (ArgumentError) prc_from_sym.('a') # => "A" prc_from_sym.('a', 'b') # => upcase': invalid option (ArgumentError)

prc_from_blk == prc_from_sym # => false prc_from_blk == :upcase.to_proc # => false

prc_from_sym == :upcase.to_proc # => true

Можно заметить, что наиболее существенная разница между обычным блоком и сущностью созданной с помощью &:symbol заключается в том, что блок требует для своей работы как минимум тот объект, на котором он должен вызвать метод upcase и не возражает, если ему передать ещё какие-то параметры. В то же время сущность, созданная с помощью &:symbol более строго относится к количеству аргументов передаваемых ей.

Из последней строчки листинга следует, что сущность, создаваемая с помощью &:symbol эквивалентна proc-объекту, возвращаемому методом Symbol#to_proc.

Теперь давайте рассмотрим более сложный пример — вызов метода с одним аргументом. Вспомним, что в Ruby оператор + реализован, как вызов метода +. Т.е. выражение 'a' + 'b' на самом деле вычисляется как 'a'.+('b') — вызов на строке 'a' метода + с аргументом 'b' (так происходит вызов метода String#+). Это сделано для того, чтобы кастомные классы могли переопределять этот метод и реализовывать нужное им поведение. Давайте посмотрим, как будет себя вести "нормальный" блок в теле которого используется + и та сущность, которая генерируется с помощью конструкции &:+ :

def met yield 'a', 'b' end

met { |x, y| x + y } # => "ab" <- вычисляется как 'a'.+('b') met(&:+) # => "ab"

Ситуация полностью аналогична вызову met(&:upcase) — сущность созданная с помощью &:symbol возвращает такой же результат, как и обычный блок.

Сравним блок с сущностью, созданной с помощью &:symbol:

prc_from_blk = blk_to_prc { |x, y| x + y } prc_from_blk.class # => Proc prc_from_blk.lambda? # => false prc_from_blk.parameters # => [[:opt, :x], [:opt, :y]] prc_from_blk.arity # => 2 prc_from_blk.() # =>

': undefined method +' for nil:NilClass (NoMethodError) prc_from_blk.('a') # => +': no implicit conversion of nil into String (TypeError) prc_from_blk.('a', 'b') # => "ab" prc_from_blk.('a', 'b', 'c') # => "ab"

prc_from_sym = blk_to_prc(&:+) prc_from_sym.class # => Proc prc_from_sym.lambda? # => false prc_from_sym.parameters # => [[:rest]] prc_from_sym.arity # => -1 prc_from_sym.() # => <main>': no receiver given (ArgumentError) prc_from_sym.('a') # => +': wrong number of arguments (given 0, expected 1) (ArgumentError) prc_from_sym.('a', 'b') # => "ab" prc_from_sym.('a', 'b', 'c') # => `+': wrong number of arguments (given 2, expected 1) (ArgumentError)

prc_from_sym == :+.to_proc # => true

Можно предположить, что сущность, создаваемая с помощью &:symbol требует чтобы ей передавалось ровно столько аргументов сколько способен принять метод имя которого задается символом в конструкции &:symbol плюс один аргумент — тот объект на котором будет вызываться заданный метод.

Для того чтобы подтвердить наше предположение давайте с помощью конструкции &:symbol сгенерируем блок с методом, который принимает более чем два аргумента:

def met0 yield [] end

def met1 yield [], 'a' end

def met2 yield [], 'a', 'b' end

def met3 yield [], 'a', 'b', 'c' end

met0 { |arr| arr } # => [] met0(&:append) # => []

met1 { |arr, x1| arr.append(x1) } # => ["a"] met1(&:append) # => ["a"]

met2 { |arr, x1, x2| arr.append(x1, x2) } # => ["a", "b"] met2(&:append) # => ["a", "b"]

met3 { |arr, x1, x2, x3| arr.append(x1, x2, x3) } # => ["a", "b", "c"] met3(&:append) # => ["a", "b", "c"]

prc_from_sym = blk_to_prc(&:append) prc_from_sym == :append.to_proc # => true

Мы можем сделать вывод, что сущность создаваемая с помощью конструкции &:symbol эквивалентна блоку, принимающему в качестве своего первого аргумента объект на котором он вызывает метод с именем задаваемым символом :symbol, передавая все остальные аргументы этому методу в качестве аргументов. Метод же Symbol#to_proc просто позволяет получить эту сущность в виде proc-объекта, не создавая для этого сервисный метод — метод blk_to_prc в нашем примере. #7. Разбираемся, как устроен метод Symbol#to_proc

Давайте попробуем реконструировать метод Symbol#to_proc. Т.е. переписать на Ruby ту словесную формулировку, к которой мы пришли в предыдущей части статьи. Вот эта формулировка:

Сущность создаваемая при вызове метода с помощью конструкции &:symbol эквивалентна блоку, принимающему в качестве своего первого аргумента объект на котором он вызывает метод с именем задаваемым символом :symbol, передавая все остальные аргументы этому методу в качестве аргументов. Метод же Symbol#to_proc просто позволяет получить эту сущность в виде proc-объекта.

Начинаем с заготовки:

class Symbol def to_proc proc { |obj, *args| }

obj — это тот объект на котором нужно вызвать заданный метод

args — массив содержащий аргументы, которые нужно передать вызываемому методу

end end

Давайте представим себе как будет выглядеть proc-объект, возвращаемый методом Symbol#to_proc для символа :upcase:

proc { |obj, *args| obj.upcase } proc { |obj, *args| obj.upcase() } # скобки только ради единообразия со последующими примерами

А вот так для символа :+:

proc { |obj, *args| obj.+(args[0]) }

Для символа :append:

proc { |obj, *args| obj.append(*args) } # сплэт-оператор * "вытряхивает" объекты из массива args

Все вышеприведенные proc-и отличаются только именем метода, который они вызывают на своем первом аргументе. Но ведь этот proc будет создаваться внутри метода экземпляра класса Symbol. А что является экземплярами этого класса? Например, вот такие символы: :upcase, :+, :append. Как можно обратиться к самому экземпляру класса из метода экземпляра этого класса? Это делается с помощью переменной self. И наконец, как вызвать на каком-то объекте метод зная имя этого метода? Мы можем это сделать с помощью метода send или с помощью метода public_send. Выберем последний для того чтобы случайно не вызвать приватный метод. Вот что у нас получилось:

class Symbol def to_proc proc { |obj, *args| obj.public_send(self, *args) } end end

Здесь у нас возникает соблазн проверить написанный нами метод Symbol#to_proc в действии. Но если мы запустим на выполнение код в таком виде, в каком он есть сейчас, то мы заменим оригинальный метод Symbol#to_proc нашим (для этого есть специальный термин — monkey patching). Этого не стоит делать, по крайней мере, по двум причинам:

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

как мы уже указывали ранее, класс Symbol входит в состав ядра Ruby и т.о. он написан на языке Си. Нельзя заранее предсказать к каким последствиям может примести замена метода реализованного на Си методом реализованном на Ruby.

Первую причину мы можем проигнорировать, т.к. мы просто хотим проверить нашу реализацию метода Symbol#to_proc. Мы не собираемся её использовать в наших проектах.

А для решения второй в Ruby есть метод Module#prepend. Он очень похож на метод Module#include, но в отличие от последнего он помещает модуль в цепочку наследования класса не после самого класса, а перед ним. Т.е. методы содержащиеся в модуле, который мы prepend-им в какой-то класс перекрывают одноименные методы, определенные в самом классе. Эта техника считается более безопасной, чем обычный monkey patching. Если это пока звучит для вас непонятно, потерпите — мы сейчас покажем, как это делается на практике.

module Procable # Создаем новый модуль def to_proc # Переносим наш метод to_proc из класса Symbol в модуль puts "My Symbol#to_proc is running for the #{inspect} symbol" # чтобы убедиться, что работает именно наш метод proc { |obj, *args| obj.public_send(self, *args) } end end

class Symbol prepend Procable # Включаем модуль с нашим методом to_proc в цепочку наследования класса Symbol ПЕРЕД самим этим классом end

Вот что сделал метод Module#prepend с цепочкой наследования класса Symbol

Symbol.ancestors # => [Procable, Symbol, Comparable, Object, Kernel, BasicObject]

words = 'one two three four five six seven'

words.split.map(&:reverse).map(&:capitalize).map(&:reverse).join(' ') # => "onE twO threE fouR fivE siX seveN" words.gsub(/\w\b/, &:upcase) # => "onE twO threE fouR fivE siX seveN"

Вывод в консоли:

My Symbol#to_proc is running for the :reverse symbol My Symbol#to_proc is running for the :capitalize symbol My Symbol#to_proc is running for the :reverse symbol My Symbol#to_proc is running for the :upcase symbol

Наш метод Symbol#to_proc вызывается четыре раза (дла раза для символа :reverse, один раз для :capitalize и один раз для :upcase) о чем и свидетельствует вывод в консоли.

Для особо внимательных людей мы хотим пояснить, что сам факт манки патчинга метода Symbo#to_proc приводит к тому, что этот метод начинает вызываться при вызове методов с аргументом вида &:symbol. Если же мы пользуемся стандартной реализацией метода Symbol#to_proc, то при вызове методов с аргументом вида &:symbol метод Symbol#to_proc не вызывается, а просто создается эквивалентный блок. Сам же стандартный метод Symbol#to_proc предназначен исключительно для того, чтобы получить этот блок в виде proc-объекта. Вы сами можете в этом убедиться с помощью трассировщика, техника использования которого подробно показана в предыдущих частях этой статьи.

Итак, мы выяснили эквивалентную Ruby-реализацию метода Symbol#to_proc и то как он участвует в генерации блока при вызове методов с аргументом вида&:symbol. И теперь мы можем сказать, что он является синтаксическим сахаром не в большей мере, чем сам язык Ruby. Он органично и логично встроен в Ruby. #8. Заключение

В этот статье мы разобрали как передать методу лямбду и proc вместо блока и когда это нужно на практике. Мы научились преобразовывать блок в proc и наоборот, а так же узнали, когда это может пригодиться. Кроме того мы затронули смежную тему — создание блока с помощью конструкции &:symbol и убедились, что в этом нет никакой магии — она органично и логично встроена в Ruby.

Что предпочесть — создание блока с помощью конструкции &:symbol или использование обычных блоков? Мы считаем, что в простых случаях (например, когда блок состоит только из вызова одного метода либо вообще не принимающего аргументов, либо с одним аргументом — такого как методы upcase и +) предпочтительнее использовать генерацию блока с помощью &:symbol. Это повышает читаемость кода, а как дополнительный бонус вы получаете увеличенную производительность. Во всех же остальных случаях стоит выбирать обычные блоки, т.к. создание эквивалентной конструкции с использованием &:symbol потребует больших усилий при создании и, что самое главное, она плохо читается — приходится подолгу всматриваться, чтобы понять, что же происходит в таком коде. Если же вы склоняетесь к использованию сложных конструкций с &:symbol чтобы улучшить производительность, то вполне может оказаться, что либо выигрыш будет слишком мал, либо его вообще может не быть. В любом случае выбор за вами.