Загрузка...

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

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

3. Metaprogramming patterns — 20 кю. Замыкания

28.02.09, 16:21
Автор artem.voroztsov

В предыдущем посте мы затронули важнейшую концепцию — замыкание (closure).
Суть этой концепции в том, что в любой блок как бы заключается «весь окружающий мир» так, как он виден в контексте, где блок создается. Правильнее сказать, что в блок заключается не весь окружающий мир (пространство имён), а фиксируется точка зрения на окружающий мир (пространство имён).

Перечитайте этот абзац ещё раз, после того как рассмотрите следующие примеры.
Для понимания примеров полезно самостоятельно познакомиться с понятием блока, методом Proc#call, конструкцией lambda, а также с понятиями переменная экземпляра класса (instancе variables — переменные, чьи имена начинаются на @) и переменная класса (class variables — переменные, чьи имена начинаются на @@):

  • Proc — класс для блоков, которые можно про себя называть неименованными (анонимными) методами, которые можно создавать прямо в выражениях;
  • Выражение b.call(*args) выполняет блок b, и возвращает результат выполнения; вместо call можно использовать квадратные скобки.
  • lambda {|a,...| ... } — создает блок, например b = lambda {|x,y,z| x+y+z} создаст блок, который складывает три числа, в частности выражение b[1,2,3] вернет 6;
  • блоки создаются не только с помощью lambda, они также конструируются автоматически при вызове метода с последующей конструкцией { ... } или do ... end; например ary.inject{|a,b| a * b} передаст внутрь метода inject блок, выполняющий умножение двух чисел;
  • instance-переменные живут в объектах и считаются инициализированными значением nil по умолчанию;
  • class-переменные живут в классах и считаются по умолчанию неинициализированными; при их использовании в выражении без предварительной инициализации возникает Exception "uninitialized class variable .. in ...";

Итак, примеры кода:
Пример 1.

a = 1
b = lambda { puts a }
b.call # напечатает 1
a = 2
b.call # напечатает 2
       # ничего страшного в этом примере нет - вполне ожидаемое поведение

Пример 2.
class Abc
  attr_accessor :bar
  def foo
    @bar ||= 0
    x = 5
    lambda { puts @bar, x, self.class; }
  end
end
x = 10
a = Abc.new
b = a.foo
b.call  # напечатает 0, 5 и Abc
a.bar += 1
x = 10
b.call  # напечатает 1, 5, и Abc
        # аттрибут bar объекта a виден из блока b,
        # сам блок (как переменная) находится в нашем контексте -- является
        # локальной переменной в нашем контексте; но он видит мир как-бы изнутри
        # функции foo, где есть @a и своя локальная переменная x,
        # которая видна и не умирает, несмотря на свою локальность
        # и тот факт, что выполнение foo давно закончилось.

Замыкание происходит для любого блока, как для созданного с помощью lambda, так и для блока, переданного методу, как оформленнного с помощью фигурных скобок, так и с помощью конструкции do ... end.

В последнем примере мы вызвали метод foo у экземпляра a некоторого класса Abc.
Внутри этого метода инициализируется переменная экземпляра @bar и возвращается блок, который печатает эту переменную, а также значение локальной переменной x и self.class.
После выполнения этого кода Вы увидите, как сильно блок привязан к своей родине, все его мысли и побуждения — там.

В контексте, в котором выполняется строка "b.call", переменная @bar не видна (лучше сказать, её просто нет в этом контексте).
Но тем не менее, выполнение блока b приводит к выводу значений переменной @bar объекта a, который, как будто бы, здесь непричём. Объясняется это тем, что блок создавался в контексте выполнения метода foo объекта a, и в этом контексте были видны все instance-переменные объекта a.

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

Пример 3.
class Abc
  attr_accessor :block
  def do_it
    @a = 1
    block.call
  end
end
c = 1
a = Abc.new
a.block = lambda { puts "c=#{c}"}
a.do_it # напечатает 1;
        # видимость локальной переменной изнутри блока - активно 
        # используемая фича в динамическом программировании
a.block = lambda { puts "@a=#{@a.inspect}"}
a.do_it # напечатает nil, т.к. @а не инициализирована в нашем контексте,
        # а именно этот контекст "заключён внутрь" блока a.block.
        # Хоть выполнение блока a.block запускается внутри метода Abc#foo
        # контекст Abc#foo неизвестен внутри блока a.block

Повторим то же самое, только теперь блок будет создаваться просто как блок, ассоциированный с методом, а не с помощью конструкции lambda:
class Abc
  def do_it(&block)
    @a = 1
    block.call
  end
end
c = 1
a = Abc.new
a.do_it {puts "c=#{c}"} 
a.do_it { puts "@a=#{@a.inspect}"}

Что такое контекст?

Это определёная точка зрения на пространство имен, из которой что-то видно, что-то невидно, а что-то видно по-своему.

Например, из тела метода видны instance-переменные того объекта, для которого этот метод виден, а self равен этому объекту. Instance-переменные других объектов не видны.

Особое выражение self полезно рассматривать как некоторый метод, который в каждом контексте может быть по-своему определён.

Повод для смены контекста — конструкции def и class. Именно они обычно приводят к смене видимости instance-переменных, class-переменных и смене значения выражения self.

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

Собственно понятие контекст имеет своё вполне конкретное отображение в Ruby — это объект класса Binding. У каждого блока есть binding. Кроме того есть метод binding, который и возвращает контекст который мы имеем в том месте, где метод binding был вызван.  Объект класса Binding можно передавать как второй аргумент методу eval («выполни данный код в таком-то контексте»):

Пример 4.

class Abc
  attr_accessor :x
  def inner_block
    lambda {|x| x * @factor}
  end
end
a = Abc.new
b = a.inner_block
eval("@factor = 7", b.binding)
puts b[10] # напечатает 70
eval("@x = 6 * @factor", b.binding)
puts a.x   # напечатает 42

Но, конечно, так писать не нужно. Для выполнения кода в контексте объекта используйте просто instance_eval:

Пример 5.
class Abc
  attr_accessor :x
end
a = Abc.new
a.instance_eval("@factor = 7")
a.instance_eval("@x = 6 * @factor")
puts a.x # напечатает 42

Час расплаты

За такое удовольствие как замыкания, нужно платить.

  • Если жива ссылка на блок, то жив соответствующий контекст и живы все объекты, которые видны из данного контекста (в первую очередь имеются в виду локальные переменные). Значит мы не имеем права собирать эти объекты сборщиком мусора. Замыкание как бы зацепило их всех разом. Для тех, кто знаком с концепцией smart pointers, можно пояснить, что создание контекста (binding) как бы приводит у увеличению ref_counter на 1 для всех видимых объектов, и соответственно при разрушении контекста (возникающее, при удалении всех блоков, созданных в данном контексте) происходить уменьшение ref_counter на 1 для всех видимых объектов. Но на самом деле этого не делается. Сборщик мусора в Ruby построен на другой концепции, отличной от smart pointers (см. Status of copy-on-write friendly garbage collector — Ruby Forum, в частности, www.ruby-forum.com/attachment/2925/mostlycopy-en.ppt, а также Memory leak in callcc)
  • Настоящие замыкания хранят в себе не только видимость пространства имён, но и стек вызовов. В Ruby можно получить доступ к стеку (см. метод caller), а значит, если мы хотим достичь абсолютной аутентичности инстанциированного контекста (объекта класса Binding) понятию реального контекста, нужно хранить и стек вызовов и все объекты, которые есть в этом стеке, и это становится реальной проблемой. Если бы даже метода caller не было, к стеку вызовов можно было бы добраться кидая Exception:

    def backtrace
      begin
        raise Exception.new('')
      rescue Exception=>e
        e.backtrace[1..-1]
      end
    end
    def f
      g
    end
    def g
      puts backtrace.join("\n")
    end
    f

    В результате вы получите вывод:
    make_rescued.rb:15:in `g'
    make_rescued.rb:11:in `f'
    make_rescued.rb:18
  • Одна из оптимизаций может заключатся в том, что анализируется код блока и не создается контекст, если в блоке не используются локальные переменные и др. Например, для выражений типа ary.map{|i| i*i} или users.map{|u| e.email}, не хотелось бы заниматься замыканиями. Но часто просто нет возможности предсказать, что из видимого пространства имён будет использоваться блоком, так как в принципе в блоке может встречаться eval или вызов метода с ассоциированным блоком, который, в свою очередь, может запросить у переданного ему блока block значение block.binding и делать с ним, что захочет. Также следуется бояться выражения send(m, *args), так как это может оказаться send('eval', *args). Есть возможность создавать блок с минимальным контекстом следующим образом: "block = class < < Object.new; lambda { ... } end". Возможно, имеет смысл для оптимизации (в первую очередь хочется избавится от цепляющегося за замыкания стека вызовов) придумать новую языковую конструкцию вида glob_do ... end для создания блоков, чей контекст общий — глобальный контекст, в котором self равно специальному объекту main.

 Ссылки

Комментарии

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