Семинар 5: Как передать функцию в функцию в Ruby

Немного теории

В Ruby есть блоки. Блок кода - это просто кусок кода, обрамленный ключевыми словами do и end или в фигурных скобках.

Блок - это не объект какого-то класса, блок - это специальная конструкция языка. Его нельзя записать в переменную, нельзя явно передать функции и т.д.

Работа с блоками - это специальный встроенный в Ruby механизм, работа с ним происходит только через определенные ключевые слова.

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

Любой функции можно передать блок кода

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

На уровне языка каждая функция умеет принимать последним неявным параметром блок кода.

def foo(x)
end

foo(1) do |x|
  x + 1
end

Мы объявили функцию foo и при вызове этой функции с аргументом 1 передали ей блок.

Он начинается ключевым словом do и заканчивается ключевым словом end.

Примечание

Бывают однострочные блоки, в данном случае он бы выглядел так:

foo(1) {|x| x + 1 }

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

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

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

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

Выше также было написано, что работа с блоками происходит через определенные ключевые слова. Существует ключевое слово для вызова выполнения блока - yield. Yield принимает аргументы и передает их в блок по порядку.

alt text

def foo(x)
  yield x # Здесь мы вызываем исполнение переданного блока с аргументом 1.
end

foo(1) do |x|
  x + 1
end

Конструкция yield x значит примерно следующее: "попытайся вызвать исполнение блока кода, переданного функции, и дай ему в качестве аргументов все то, что стоит после yield" (как вы помните, скобки ставить не обязательно, а если бы они стояли, то это выглядело бы как yield(x)).

Конструкция

[1, 2, 3].map {|x| x**2}

это и есть передача блока методу map, который внутри себя делает yield!

yield - достаточно тупая инструкция. Если вызывать выполнение блока, которого нет, будет ошибка.

Что же сделать, чтобы не упало?

Проверить наличие блока:

def smart_foo(x)
  if block_given?
    yield x
  else
    p 'Блок не передан'
    x
  end
end

smart_foo(1) do |x|
  p 'Блок передан'
  x + 1
end

smart_foo(1)

Если напутать с количеством параметров, которые умеет принимать блок, ничего криминального не случится.

Здесь мы передаем в функцию блок, который не принимает ни одного параметра (после do нет `| ... |'). При вызове блока мы тем не менее передаем в него параметр х. Несмотря на то, что есть очевидная несостыковка, блоки в Ruby построены таким образом, что лишние параметры будут просто проигнорированы.

def foo(x)
  yield x
end

foo(1) do
  p 'У меня нет параметров'
end

Практический пример

Возьмем массив слов. Отсортируем его с помощью классической функции.

array = ['aaa', 'b', 'ccc']
array.sort

Тут происходит сортировка в лексикографическом порядке, то есть слово, начинающееся с буквы a, меньше слова, начинающегося с буквы b, какие бы дальше буквы ни шли.

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

array.sort {|x, y| x.length <=> y.length}

Но что делать, если блока недостаточно?

Почему вообще нас может не устраивать блок?

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

Proc (от Procedure object)

Proc - это обертка над блоком кода, которая умеет себя вызывать через специальный метод call.

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

Также существует еще один способ объявления Proc - с помощью конструкции с ключевым словом proc:

def foo(x, f)
  f.call x
end

# Первый способ - через конструктор с блоком.
proc1 = Proc.new do |x|
  x + 2
end

# Второй способ - через специальную конструкцию языка.
proc2 = proc do |x|  
  x + 2
end
  
# Блоки в данном случае могут быть как однострочными, так и многострочными. Вот это делает абсолютно то же самое, 
# что и в примерах выше.
proc1 = Proc.new {|x| x + 2}

# Второй способ - через специальную конструкцию языка.
proc2 = proc {|x| x}

Proc можно также передавать в функцию через переменные.

p foo(1, proc1)
foo(1, proc2)

Можно передать анонимную функцию - lambda

Лямбда - это концепция, пришедшая из функционального программирования. Лямбда - это анонимная функция.

Что это вообще значит?

Вот такая функция не анонимная, она объявлена через специальный синтаксис, ей в соответствие поставлено имя.

def explicit_function()
    # Какой-то код
end

А вот такая функция уже будет анонимной

x = ->(arg) {x + 1}

Для объявления анонимной функции в Ruby есть два варианта синтаксиса:

lambda_function1 = ->(x) { x + 1 }
lambda_function2 = lambda {|x| x + 1 }

Как и в случае с Proc, для вызова лямбды нужно использовать специальный метод call

p lambda_function1.call 1
lambda_function2.call 1

Но разве эта функция анонимная, если к ней можно обращаться по имени?

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

При желании можно сразу сконструировать лямбду при вызове функции или передать переменную, содержащую ссылкку на нее:

def foo(x, l)
  l.call x
end

p foo(1, lambda_function2)
foo(1, ->(x) {x + 1})

Примечание

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

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

Немного майндфака

Если вам надоело писать суммирование массива так

[1, 2, 3].inject 0 {|sum, x| sum += x}

То можно сделать это гораздо проще

[1, 2, 3].inject &:+

Результат будет такой же.

Как это работает

Начнем с конца. :+ - это экземпляр класса Symbol. Symbol - это неизменяемая строка. То есть :+ - это просто строчка, содержащая знак +.

Symbol в Ruby используется очень широко. В этом языке нет возможности передать имя функции таким образом

def foo()
end

another_function(foo) # Так нельзя

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

Чтобы обойти это ограничение языка, для передачи имени функции стали использовать класс Symbol. Это просто константная строчка, которая содержит имя вызываемой функции. Так как она константная,то гарантируетя то, чтомы всегда будем ссылаться на одну ту же функцию.

То есть получается :+ - это имя функции, а точнее метода (а в Ruby + может быть именем функции).

Но что с того, что у нас есть имя функции? Само по себе это бесполезно. НО

Symbol умеет преобразовываться в Proc

def function(x)
  x**2
end

f = :function.to_proc

В ООП в целом и в Ruby в частности существует концепция передачи сообщений между объектами. Мы привыкли, что у объекта просто есть некоторый публичный интерфейс, который задает допустимые "ручки" (то есть методы) для взаимодействия с объектом. Однако если посмотреть на ООП более в более общем свете, становится очевидно, что методы - это некое сужение концепции, которое нужно для инженерной реализации абстрактной концепции. Примерно как с реляционной алгеброй и реальными СУБД.

С точки зрения концепции, объекты должны хранить внутри себя некое состояние и отвечать или не отвечать на воздействия внешней среды. То есть получается, в объект может приходить все, что угодно, он сам должен решать, реагировать на это или нет. Отсюда и появилась идея посылки сообщений.

В Ruby эта идея работает как некая база. Мы пользуемся обычной нотацией для вызова методов, например '1 2 3'.split, но под капотом интерпретатор Ruby как бы посылает сообщение с текстом :split (и аргументами, если они переданы) объекту '1 2 3' класса String. Это базовая процедура, в соответствии с которой работает интерпретатор, поэтому это не тормозит выполнение программы, даже наоборот, такой способ вызова методов в Ruby один из самых быстрых.

Посылка сообщений происходит в основном неявно, но при необходимости мы можем сделать это напрямую. У всех наследников класса Object есть метод send, который принимает в качестве первого аргумента название сообщения, или имя метода, а последующими - аргументы (если они нужны).

Так, вызов '1,2,3'.split эквивалентен '1,2,3'.send(:split, ','). Вы можете так делать, но смысла в этом немного - читаемость хуже, а скорость выполнения такая же. Однако зачем-то же это нужно, скажете вы. И правда, эта штука позволяет делать некоторые хитрые везщи. Например, метод Symbol#to_proc делает не что иное как

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

Этот код возвращает Proc, который принимает на вход объект и возможно аргументы. С помощью механизма отправки сообщений этот Proc посылает объекту сообщение с текстом текущего Symbol'a (self ссылается на значение текущего объекта).

Что такое &

& имеет смысл только в определенном контексте. Его нельзя писать где угодно.

В данном случае (то есть при передаче параметров в функцию) это указание интерпретатору преобразовать Symbol, который содержит имя функции, в Proc.

Как вы помните, любой метод неявно принимает блок кода. Однако можно явно указать Ruby, что мы хотим принять последний (именованный нами параметр) как блок кода. Для этого нужно поставить & перед ним.

def foo(arg, &block)
  block.call arg if block_given? or nil
end

def block arg
  arg * 2
end

foo 10, &method(:block)

Интересная задачка - объяснить, как это работает

def blah(&block)
  yadda(block)
end
 
def yadda(block)
  foo(&block)
end
 
def foo(&block)
  block.call
end
 
blah() do
  puts "hello"
end

Ответ тут

Appendix 1 - Отличия lambda и proc

Формально lambda и Proc - объекты одного класса.

l = ->() { p 'lambda' }
pr = proc { p 'proc' }
  
p l.class
p pr.class

Однако различия все же есть, для этого даже добавили специальный метод

p l.lambda?
pr.lambda?

А в чем же отличие?

Передача параметров

Если лямбда вызывается с большим, или меньшим количеством аргументов, чем необходимо, тогда Ruby выдает ошибку ArgumentError.

Однако когда Proc вызывается с большим количеством аргументов, чем необходимо, никакой ошибки не возвращается и лишние аргументы просто отбрасываются. Когда процедура вызывается с меньшим количеством аргументов, то те параметры, которые не получили необходимых значений, приобретают значение nil.

l.call 10, 11
    ArgumentError: wrong number of arguments (given 2, expected 0)

    <main>:in `block in <main>'

    <main>:in `<main>'

    /var/lib/gems/2.3.0/gems/iruby-0.3/lib/iruby/backend.rb:44:in `eval'

    /var/lib/gems/2.3.0/gems/iruby-0.3/lib/iruby/backend.rb:44:in `eval'

    /var/lib/gems/2.3.0/gems/iruby-0.3/lib/iruby/backend.rb:12:in `eval'

    /var/lib/gems/2.3.0/gems/iruby-0.3/lib/iruby/kernel.rb:87:in `execute_request'

    /var/lib/gems/2.3.0/gems/iruby-0.3/lib/iruby/kernel.rb:47:in `dispatch'

    /var/lib/gems/2.3.0/gems/iruby-0.3/lib/iruby/kernel.rb:37:in `run'

    /var/lib/gems/2.3.0/gems/iruby-0.3/lib/iruby/command.rb:70:in `run_kernel'

    /var/lib/gems/2.3.0/gems/iruby-0.3/lib/iruby/command.rb:34:in `run'

    /var/lib/gems/2.3.0/gems/iruby-0.3/bin/iruby:5:in `<top (required)>'

    /usr/local/bin/iruby:23:in `load'

    /usr/local/bin/iruby:23:in `<main>'
pr.call 10, 11
new_pr = proc {|x, y| p x, y }
new_pr.call 1

Возврат значений

Lambda - все ок

def foo
  l = ->(x) { return x }
  x = l.call 1
  "Я возвращаю X из foo: #{x}"
end

foo
    "Я возвращаю X из foo 1"

Proc - WTF?!

def foo
  pr = proc {|x| return x }
  x = pr.call 1
  "Я возвращаю X из foo: #{x}"
end

foo
    1

Только что на ваших глазах Proc прервал выполнение функции foo! Как ни странно, если убрать return, все будет работать ок.

def foo
  pr = proc {|x| x }
  x = pr.call 1
  "Я возвращаю X из foo: #{x}"
end

foo
    "Я возвращаю X из foo: 1"

Если копнуть глубже

То можно почитать это