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

    В предыдущем посте мы затронули важнейшую концепцию — замыкание (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.

    Ссылки


    • +22
    • 3,9k
    • 8
    Поделиться публикацией
    Комментарии 8
      0
      не хватает отличий между Proc и lambda(а это очень важно!) но в целом плюсик
        0
        Может, таки перенести цикл в блог Ruby?
        +2
        Спасибо! Только вот здесь
        например a = lambda {|a,b,c| a+b+c } создаст блок, который складывает три числа, в частности выражение b[1,2,3] вернет 6;

        лучше пару имён поправить (первую a и последнюю b на что-то другое)
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Да, есть такая возможность. Код
             class A; def a;  puts "A#a";  end; end
             class B < A;  def a; puts "B#a";  lambda { super };  end; end 
             B.new.a.call
            

            выведет
             B#a
             A#a
            

            Термин «переменные родителя» мне непонятен. Переменные объекта не знают к какому из классов в иерархии наследования они относятся. И никто не знает. То же самое касается переменных класса.
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Что-то не очень нравитсяопределение в википедии.
              Я бы написал

              Замыкание — это функция, которая создается во время работы программы. Из тела этой функции доступны все переменные и функции, которые дотсупны в контексте, в котором создано замыкание. Для корректной работы замыканий необходимо, чтобы при жизни замыкания жили и локальные переменные, доступные из нее. В идеале в теле замыкания должна быть возможность делать всё, что можно делать в контексте, в котором оно было создано.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое