📜 ⬆️ ⬇️

2. Metaprogramming patterns - 22yu. Reuse in small - bang!

This time I’ll talk not only about metaprogramming, but also about Ruby, as well as about algorithms - today we’ll recall the classics and see how it appears to us in the Ruby lines of the qsort method implementation. The blog is just beginning, it’s too early to talk about real meta-programming, so I’ll allow myself to deviate from the main topic.


Previous part:
1. Metaprogramming patterns - 25 kyu. Eval method

bang methods


In the Ruby language, some methods have two variants: the mapping method and the meta-transforming method . The mapping method creates a new object, and the transform method transforms the object in place. The names of these methods are given the same, only to the latter is added to the end of the character '!' (bang !!).
')
Examples:
We have similar pairs for the select methods (filter elements by a given filter), map (convert array elements according to a given function) and flatten (expand nested arrays so that we obtain a one-dimensional array whose elements are not arrays).
Such pairs of methods are found not only for arrays, but also in other classes. For rows, we have downcase and downcase! , upcase and upcase! , sub and sub! (replacement of the first found substring by sample), gsub and gsub! (replacing all found substrings), strip and strip! (removal of extreme whitespace characters), ...

Let's write the make_nobang method



In consolidating the material of the previous post, we will write a method that defines the methods. It will be called make_nobang .

Imagine that you are a programmer who has been assigned to program the above methods of the class String. Of course, these are too important methods to program in Ruby, not in C (core Ruby classes are written in C). But nevertheless, let's see how one could get the methods downcase , upcase , sub , strip , gsub , having the methods downcase! upcase! sub! strip! , gsub! :
 class String
   def downcase!
      ...
   end
   def upcase!
      ...
   end
   def sub!
      ...
   end
   def strip!
      ...
   end
   make_nobang: downcase,: upcase,: sub,: gsub,: strip
 end

You only need to implement the make_nobang method:
 class Module
   def make_nobang (* methods)
     methods.each do | method |
       define_method ("# {method}") do | * args |
         self.dup.send ("# {method}!", * args)
       end
     end
   end
 end

The verification code may, for example, be:
 class String
  def down!
    self.downcase!
  end
  make_nobang: down
 end

 a = "abcABC"
 puts a.down
 puts a
 a.down!
 puts a


Why am I writing all these simple, in principle, things? I have at least three points:
1) The send method . Get familiar with the send method. You can call the xyz method on the method not only directly: a.xyz(1, 2) , but also with the help of “passing a message to the object”: a.send('xyz' ,1, 2) . The principal difference is that the first argument in the latter case can be calculated by the expression. There is another difference - send ignores the protected and private scopes. The send method is the next important dynamic programming method along with the already mentioned methods eval, class_eval, instance_eval, instance_variable_get, instance_variable_set, define_method.

2) Do not be afraid to reuse in small. This is normal. Writing 5 * 3 similar lines with your hands instead of one programmer is easy. But you need to understand an important thing: programming as an activity comes down to reading, writing, speaking and listening. Just imagine that instead of the familiar phrase "Bon appetit" you will hear "In connection with our joint eating and my benevolent attitude towards you, I wish this food to be pleasant to you and ultimately was successfully digested." Or, instead of “add a callback for my handler,” it will be pronounced “add one more handler argument to your foo function, which will be of the type bar as this function, and this argument will be used to call a function on it for each iterated object in the foo function loop” . Slang is introduced not only for the sake of brevity, but also to simplify communication and mutual understanding. This allows for a kind of micro-meta-system transitions at the level of the programmer’s thinking.

And finally:
3) In fact, this is not easy, and the code is not very good and, moreover, in some cases does not work. Example:
 class Array
  def m! (& block)
    self.map! (& block)
  end
  make_nobangs: m
 end

 a = [1,2,3]
 puts (am {| i | i * i}). inspect
 puts a.inspect

As a result, you will get
 avoroztsov @ subuntu: ~ / meta-lectures $ ruby ​​-v make_nobang.rb
 ruby 1.8.6 (2007-09-24 patchlevel 111) [i486-linux]
 make_nobang.rb: 27: in `map! ': no ​​block given (LocalJumpError)
         from make_nobang.rb: 27: in `m! '
         from make_nobang.rb: 6: in `send '
         from make_nobang.rb: 6: in `m '
         from make_nobang.rb: 33
 avoroztsov @ subuntu: ~ / meta-lectures $ 

The implementation of make_nobang bad because
1) the signature (here I mean only the number of arguments) of the method obtained is different from the signature of the original
2) does not work if the method receives a block
3) makes a method with a public scope, although the source one probably had the appearance of private or protected .

So here!
On the one hand, this is a reason to say that all this is nonsense and it is easier for each method to write its own 3 lines.
On the other hand, this is just a reason to make such a method make_nobang , so that it really make_nobang into account all the subtleties, and so that when changing the signature and visibility of the bang-method, it is not necessary to make the appropriate changes to the nobang-method. In addition, make_nobang calls can be processed by an automatic documentation system.

Point 2 is corrected by time. The following code works in the new version of Ruby:
 class Module
   def make_nobangs (* methods)
     methods.each do | method |
       define_method ("# {method}") do | * args, & block |
         self.dup.send ("# {method}!", * args, & block)
       end
     end
   end
 end


Point 3 is solved. See methods private_methods , protected_methods , ... for class Object .

Point 1 is also solved. At least it is solved with the help of eval . See Discussion Method # get_args where you can fully get an idea of ​​what a method signature is in Ruby.

Make_bang method



Methods sort and sort! already have arrays. But let's make sure that this post isn't in vain, write a quick sort on Ruby yourself and implement the qsort and qsort! methods qsort!

Method 1



Try using the partition method defined for Enumerable instances:
 class Array 
    def qsort
       return self.dup if size <= 1
       # we will divide into parts by the first element
       l, r = partition {| x |  x <= self.first}
       c, l = l.partition {| x |  x == self.first}
       l.qsort + c + r.qsort # concatenation of three arrays
    end
 end


Method 2


It is convenient to divide the source array into three arrays at once. To do this, we define the partition3 method:

 class Array 
    # given block should return 0, 1 or 2
    # -1 stands for 2
    # outputs three arrays
    def partition3
       a = Array.new (3) {| i |  []}
       each do | x |
          a [yield (x)] << x
       end
       a
    end
    def qsort
       return self.dup if size <= 1
       c, l, r = partition3 {| x |  first <=> x}
       l.qsort + c + r.qsort
    end
 end


A version of the sort function is also needed, which sorts the array in place. Here she is:
 class Array
    def qsort!
       self.replace (self.qsort)
    end
 end
 a = [1,7,6,5,4,3,2,1]
 p a.qsort # => [1, 1, 2, 3, 4, 5, 6, 7]
 pa # => [1,7,6,5,4,3,2,1]
 a.qsort!
 pa # => [1, 1, 2, 3, 4, 5, 6, 7]


But the same thing could be done without neglecting metaprogramming:
 def make_bang (* methods)
   methods.each do | method |
     define_method ("# {method}!") do | * args |
       self.replace (self.send (method, * args))
     end
   end
 end
 class Array
   make_bang: qsort
 end


PS:


I must say that I came up with the make_nobang and make_bang methods make_nobang and there’s make_bang nothing like that yet in core and std, and never will be in the near future. :)))
This was again a purely academic example.

PSS: Understanding and Challenges



1. Why have a class Set Set no method " sort! "? Why do different classes (for example, Float Float ) there is no " to_i! " to_i! ?
2. Why is there no unary operator " ++ "?
3. What should be done more correctly: do the nobang-method from the bang-method or vice versa?
4. How are the code lines different?
a) a = a.sort.select{|x| x > 0}.uniq a = a.sort.select{|x| x > 0}.uniq ;
b) a.uniq!; a.select!{|x| x > 0}.sort! a.uniq!; a.select!{|x| x > 0}.sort! ;
c) a.uniq!.select!{|x| x > 0}.sort! a.uniq!.select!{|x| x > 0}.sort! ?
Which option is more correct?
5. Try to write the most correct make_nobang .

6. Compare two codes:
 class Module
   def make_nobang (* methods)
     methods.each do | method |
       bang_method = "# {method}!"
       define_method ("# {method}") do | * args |
         self.dup.send (bang_method, * args)
       end
     end
   end
 end

and
 class Module
   def make_nobang (* methods)
     methods.each do | method |
         define_method ("# {method}") do | * args |
         self.dup.send ("# {method}!", * args)
       end
     end
   end
 end

Does the first code work? Is the local variable bang_method from the method being created? If available, is it a miracle? She is local! And the created method will be called later, when the make_bang method has finished its execution!
If all the same both ways work, but which of them is more effective?

Source: https://habr.com/ru/post/50169/


All Articles