Загрузка...

Ruby & Rails: веб-разработка с удовольствием

Ruby on Rails — фреймворк для создания веб-приложений. Является открытым программным обеспечением (лицензия MIT). Здесь мы обсуждаем новости RoR, делимся учебными материалами и интересными находками С RoR даже сложные веб-приложения могут быть написаны за считанные дни. Это действительно разработка с удовольствием!
     

block, proc и lambda в Ruby

13.07.10, 12:36
Автор ggg

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

 

Итак, начнем с блоков. В языке программирования Ruby имеется достаточно большое число функций высшего порядка, таких как map, each, find и т.п. Суть их, как можно понять из названия, состоит в отображении массива данных, прохождении по каждому из элементов массив, нахождении определенного элемента и так далее. Имена функций отвечают на вопрос, что мы хотим сделать, но не отвечает на вопрос - как это сделать: например, в функции поиска мы подразумеваем, что хотим найти один из элементов массива, но в названии не указано, каким критериями должен обладать нужный нам элемент. Так как массивы могут содержать элементы совершенно различных типов, таких как строки, числа или более сложные объекты, но написать заранее все возможные функции поиска не представляется возможным. Вот тут-то нам и приходят на помощь блоки. Блок - это обособленный набор инструкций, который может обращаться к переменным контекста, из которого он создан. Приведем пример использования блока:

[1, 2, 3].map do |i|
  i + 1
end

В данном примере вызывается функция map, которая берет каждый из элементов массива и передает его на вход блоку

do |i|
  i + 1
end

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

[2, 3, 4]

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

Теперь рассмотрим пример своей функции, использующей блок.

def reverse_map(array)
  if block_given?
    array.reverse.map do |i|
      yield(i)
    end
  end
end
reverse_map([1, 2, 3]) do |i|
  i + 1
end

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

[4, 3, 2]

Как можно увидеть из кода примера, для вызова блока, переданного функции используется конструкция yield, а для того, чтобы понять, был ли передан на вход функции блок используется функция block_given? (не забудте про знак вопроса на конце).

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

def foo
  a = 10
  yield
end
a = 5
foo do
  puts a
end

В результате выполнения программы будет на экран будет выведено число 5, а не 10, потому что блок определен вне функции foo, а вне функции foo переменная a принимает значение 5.

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

class Server
  def initialize(&block)
    @data_processor = block
  end
  def run
    while true
      # reading data
      @data_processor.call(data)
    end
  end
end
server = Server.new do |data|
  # process data here
end
server.run

Как можно увидеть из примера в конструкторе класса Server мы передаем на вход переменную block со знаком амперсанда и Ruby автоматически делает объект класса Proc из переданного на вход блока. После чего, мы можем поместить полученный объект в локальную переменную и вызвать его при обработки данных во время чтения. Существует еще несколько способов создания объекта Proc:

p = proc {puts hello}
p.call
p = Proc.new {puts hello}
p.call

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

class Server
  def initialize(callbacks)
    @callbacks = callbacks
  end
  def run
    while true
      # reading data
      if read_success?
        @callbacks[:read_success].call(data)
      else
        @callbacks[:error_fond].call(error)
      end
    end
  end
end
callbacks = {
  :read_success => proc { |data| puts "Processing data" },
  :error_found => proc { |error| puts "Error found!" }
}
server = Server.new(callbacks)
server.run

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

Таким образом, объекты Proc соответствуют тому, что в других языках называется анонимными функциями или lambda. В Ruby тоже имеются lambda-фукнции, рассмотрим пример использования:

def foo(fun)
  fun
end
foo(lambda { puts "Hello" })

На первый взгляд, lambda ничем не отличается от Proc-а, но на самом деле, несколько различий есть:

 

  1. lambda-функции больше похожи на обычный метод и поэтому накладывают дополнительные ограничения на входные параметры: если lambda функция объявлена с двумя параметрами - то при вызове на вход должно быть передано именно 2 параметра, в противном случае будет сгенерировано иключение ArgumentError, в случае Proc-а лишние параметры будут отброшены, а недостающие заполнены значением nil.
  2. lambda-функции похожи на обычные еще и тем, что вызов конструкции return приводит к выходу из lambda-функции, а не из родительской
Рассмотрим два примера, которые расставят все по своим местам:
def args(code)
  one, two = 1, 2
  code.call(one, two)
end
args(Proc.new{|a, b, c| puts "Аргументы #{a}, #{b} и #{c.class}"})
args(lambda{|a, b, c| puts "Аргументы #{a}, #{b} и #{c.class}"})
# => Аргументы 1, 2 и NilClass
# ArgumentError: wrong number of arguments (2 for 3) (ArgumentError)
def proc_return
  Proc.new { return "Proc.new"}.call
  return "proc_return метод завершился"
end
def lambda_return
  lambda { return "lambda" }.call
  return "lambda_return метод завершился"
end
puts proc_return
puts lambda_return
# => Proc.new
# => lambda_return method finished
В заключение, хотелось бы отметить, что использование приведенных выше средств в своих программах на Ruby делает код более понятным и позволяет создавать красивые решения сложных проблем.

Комментарии

Войдите, чтобы оставить комментарий