В предыдущем посте мы затронули важнейшую концепцию — замыкание (closure).
Суть этой концепции в том, что в любой блок как бы заключается «весь окружающий мир» так, как он виден в контексте, где блок создается. Правильнее сказать, что в блок заключается не весь окружающий мир (пространство имён), а фиксируется точка зрения на окружающий мир (пространство имён).
Перечитайте этот абзац ещё раз, после того как рассмотрите следующие примеры.
Для понимания примеров полезно самостоятельно познакомится с понятием блока, методом
Итак, примеры кода:
Пример 1.
Пример 2.
Замыкание происходит для любого блока, как для созданного с помощью
В последнем примере мы вызвали метод
Внутри этого метода инициализируется переменная экземпляра
печатает эту переменную, а также значение локальной переменной
После выполнения этого кода Вы увидите, как сильно блок привязан к своей родине, все его мысли и побуждения — там.
В контексте, в котором выполняется строка "
Но тем не менее, выполнение блока
Таким образом, внутренний контекст объекта можно вытащить наружу с помощью блока, созданного внутри объекта и переданного как результат некоторой функции наружу.
Пример 3.
Повторим то же самое, только теперь блок будет создаваться просто как блок, ассоциированный с методом, а не с помощью конструкции
Это определёная точка зрения на пространство имен, из которой что-то видно, что-то невидно, а что-то видно по-своему.
Например, из тела метода видны instance-переменные того объекта, для которого этот метод виден, а
Особое выражение
Повод для смены контекста — конструкции
Обычный блок также является новым контекстом, пусть и включающим в себя контекст, в котором был создан. В блоке могут быть свои локальные переменные (также как в Си) и аргументы (которые следует интерпретировать как особые локальные переменные).
Собственно понятие контекст имеет своё вполне конкретное отображение в
Пример 4.
Но, конечно, так писать не нужно. Для выполнения кода в контексте объекта используйте просто
Пример 5.
За такое удовольствие как замыкания, нужно платить.
Суть этой концепции в том, что в любой блок как бы заключается «весь окружающий мир» так, как он виден в контексте, где блок создается. Правильнее сказать, что в блок заключается не весь окружающий мир (пространство имён), а фиксируется точка зрения на окружающий мир (пространство имён).
Перечитайте этот абзац ещё раз, после того как рассмотрите следующие примеры.
Для понимания примеров полезно самостоятельно познакомится с понятием блока, методом
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
.
Ссылки
- www.javapassion.com/rubyonrails/ruby_meta.pdf — презентация указывает на множество вещей, о которых я собираюсь писать в этом блоге
- www.infoq.com/presentations/nutter-jruby-jvm-lang-summit — The Pain of Bringing an Off-Platform Dynamic Language to the JVM — рассказывается о проблемах написания компилятора в Java byte-код для динамических языков, даёт представлении вообще о задачах решаемых при написании интерпретаторов динамических языками
- 1. Metaprogramming patterns — 25кю. Метод eval
- 2. Metaprogramming patterns — 22кю. Reuse в малом — bang!