3. Metaprogramming patterns - 20 kyu. Short circuits

    In the previous post, we touched on the most important concept - closure.
    The essence of this concept is that in any block, as it were, “the whole surrounding world” as it is visible in the context where the block is created. It is more correct to say that not the whole world (namespace) is contained in the block, but the point of view on the world (namespace) is fixed.


    Reread this paragraph again after reviewing the following examples.

    To understand the examples, it is useful to independently get acquainted with the concept of the block, method Proc#call, designlambda, as well as with the concepts of a class instance variable (instanc variables are variables whose names begin with a dog) and a class variable (class variables are variables whose names begin with two dogs):
    • Proc - a class for blocks that can be called unnamed (anonymous) methods that can be created directly in expressions;
    • The expression b.call(*args)executes the block b, and returns the result of the execution; instead of call, you can use square brackets.
    • lambda {|a,...| ... }- creates a block, for example, b = lambda {|x,y,z| x+y+z}creates a block that adds up three numbers, in particular, the expression b[1,2,3]will return 6;
    • blocks are not only created using lambda, they are also constructed automatically when a method is called, followed by { ... }or do ... end; for example, ary.inject{|a,b| a * b}pass a injectblock inside the method that multiplies two numbers;
    • instance variables live in objects and are considered initialized to nil by default;
    • class variables live in classes and are considered uninitialized by default; when used in an expression without preliminary initialization, a Exception" uninitialized class variable .. in ..." occurs ;

    So, the code examples:

    Example 1. Example 2. Closing occurs for any block, both created using , and for the block passed to the method, decorated using curly braces, as well as using the construction . In the last example, we called the method on instance a of some class . Inside this method, an instance variable is initialized and a block is returned that prints this variable, as well as the value of the local variable and . After executing this code, you will see how much the block is attached to its homeland, all its thoughts and motives are there. In the context in which the string " " is executed , the variable
    a = 1

    b = lambda { puts a }

    b.call # напечатает 1

    a = 2

    b.call # напечатает 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 давно закончилось.




    lambdado ... end

    fooAbc
    @bar
    xself.class


    b.call@barnot visible (better to say, it simply does not exist in this context).
    Nevertheless, the execution of the block bleads to the derivation of the values ​​of the variable of the @barobject a, which, as if, is not appropriate here. This is explained by the fact that the block was created in the context of the execution of the method of the fooobject a, and in this context all the instance variables of the object were visible a.

    Thus, the internal context of the object can be pulled out using the block created inside the object and transferred as a result of some function to the outside.

    Example 3. We repeat the same thing, only now the block will be created simply as the block associated with the method, and not using the construct :
    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}"}





    What is context?


    This is a certain point of view on the namespace, from which something is visible, something is invisible, and something is seen in its own way.

    For example, instance-variables of the object for which this method is visible and selfis equal to this object are visible from the body of the method . Instance variables of other objects are not visible.

    It is selfuseful to consider a particular expression as some method, which in each context can be defined in its own way.

    Reason for changing context - design defand class. They usually lead to a change in the visibility of instance variables, class variables and a change in the value of an expression self.

    A regular block is also a new context, albeit including the context in which it was created. A block can have its own local variables (as well as in C) and arguments (which should be interpreted as special local variables).

    Actually, the concept of context has its own very specific mapping in Ruby- it is an object of the class Binding. Each block has one binding, and this bindingone can be passed as the second argument to the method eval: “execute the given code in this context”:

    Example 4. But, of course, you don’t need to write like that. To execute code in the context of an object, use simply : Example 5.
    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


    class Abc

      attr_accessor :x

    end



    a = Abc.new

    a.instance_eval("@factor = 7")

    a.instance_eval("@x = 6 * @factor")

    puts a.x # напечатает 42


    Reckoning hour


    For such pleasure as closures, you have to pay.
    • If the link to the block is alive, then the corresponding context is alive and all objects that are visible from this context are alive (first of all, local variables are meant). So we do not have the right to collect these objects with the garbage collector. The closure seemed to hook them all at once. For those who are familiar with the concept of smart pointers, it can be explained that creating a context (binding) seems to result in an increase in ref_counter by 1 for all visible objects, and, accordingly, when the context is destroyed (arising when all blocks created in this context are deleted) decrease ref_counter by 1 for all visible objects. But actually this is not done. Ruby's garbage collector is built on a different concept than smart pointers (see Status of copy-on-write friendly garbage collector - Ruby Forumin particular www.ruby-forum.com/attachment/2925/mostlycopy-en.ppt , as well as Memory leak in callcc )
    • Real closures contain not only the visibility of the namespace, but also the call stack. In Ruby, you can access the stack, which means that if we want to achieve the absolute authenticity of the instantiated context (class object Binding) with the concept of a real context, we need to store both the call stack and all the objects that are in this stack, and this becomes a real problem. An example of accessing the call stack: As a result, you get the output:

      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
      
    • One of the optimizations may be that the code of the block is analyzed and the context is not created if local variables, etc. are not used in the block. For example, for expressions like ary.map{|i| i*i}or users.map{|u| e.email}, I would not want to deal with closures. But often it’s simply not possible to predict what the block will use from the visible namespace, since, in principle, the block can evaleither call a method with an associated block, which, in turn, can request a blockvalue from the block passed to it block.bindingand do with it that wants to. One should also be afraid of expression send(m, *args), as this may turn out to be send('eval', *args). It is possible to create a block with minimal context as follows: "block = class << Object.new; lambda { ... } end". Perhaps it makes sense for optimization (first of all, I want to get rid of the call stack that clings to the closures) to come up with a new language construct glob_do ... endfor creating blocks whose context is common - the global context, in which it selfis equal to a special object main.

    References



    Also popular now: