Pull to refresh

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

Ruby
В предыдущем посте мы затронули важнейшую концепцию — замыкание (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<br>
b = lambda { puts a }<br>
b.call # напечатает 1<br>
a = 2<br>
b.call # напечатает 2<br>
# ничего страшного в этом примере нет - вполне ожидаемое поведение<br>

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

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

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

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

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

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


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


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


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

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

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

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

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

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

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


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

Пример 5.
class Abc<br>
attr_accessor :x<br>
end<br>
<br>
a = Abc.new<br>
a.instance_eval("@factor = 7")<br>
a.instance_eval("@x = 6 * @factor")<br>
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 можно получить доступ к стеку, а значит, если мы хотим достичь абсолютной аутентичности инстанциированного контекста (объекта класса Binding) понятию реального контекста, нужно хранить и стек вызовов и все объекты, которые есть в этом стеке, и это становится реальной проблемой. Пример доступа к стеку вызовов:

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

    В результате вы получите вывод:
    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.

Ссылки


Tags:rubymetaprogrammingпрограммированиеметапрограммированиефункциональное программированиеfunctional programming
Hubs: Ruby
Total votes 32: ↑27 and ↓5+22
Views4.9K

Popular right now