📜 ⬆️ ⬇️

1. Metaprogramming patterns - 25 kyu. Eval method

Programming, which I occasionally continue to do, is gradually changing its style and is increasingly associated with metaprogramming. At the same time, it is impossible to say that normal programming is disgusted with me. Just like any programmer, I am looking for ways to become increasingly modular, short, intelligible, and flexible code, and in metaprogramming I see an untapped potential (despite the long-term endless Internet flood on metaprogramming going from Lisp). :)

I want to start a blog dedicated to Ruby metaprogramming.

The choice of Ruby is connected with the fact that the culture of metaprogramming in the environment of Ruby programmers has already been largely formed, and the elements of metaprogramming have become the fabric of the daily work of the Ruby programmer, and, besides, he is better known to me than other dynamic languages.

I read and lecture on Ruby & Rails & Metaprogramming at the Physics and Technology Institute; materials of one of the lectures can be found here . It is briefly about the main thing in the pictures.
')
In this blog I will try to present the topic consistently and in detail. I take a deep breath in advance, because the task is not simple. I hope for your encouraging feedback.

I'll start with a simple one - with a definition.

Metaprogramming in scripting languages ​​is a style of writing programs, which usefully uses the ability to change the namespace at runtime.

The namespace refers to classes, methods, and variables (global, local, instance variable, and class variables). Change means creating, changing, and deleting classes, methods, and variables.

It must be said that in most scripting languages ​​the namespace is constructed in no other way than in runtime mode. But many do not remember this, so I emphasized it in the definition. If we remove the unnecessary mention of runtime from the definition, then the phrase “with benefit” will remain. So the essence of it.

An example of useless programming: eval "s = 'eval s'; eval s"

Calculator



I remember how in deep childhood I wrote for BK-0010 a program for plotting functions. The functions are hardcoded and when the program was running you could only select one function from the list and specify the range [x0, x1], and the range along the Y axis (about the miracle of programmer thought! Capable of automating everything and everyone) was automatically selected by the program.

I looked at my program in BASIC and experienced ecstasy. But then a sad thought came to me: “Ehh !!! And it’s a pity all the same, that it’s impossible to drive a formula of the function I need right during the program’s execution.

NDA ... 8th grade, 1992, Kirovo-Chepetsk. A lot of water has flowed since then, but the problems are the same!

Why am I doing this?

Here is the code for an interactive “calculator” in Ruby:

 while line = readline
   puts eval (line) .inspect
 end

or better
 while (print ">"; true) and line = readline
   puts eval (line) .inspect
 end

or right
require ' readline '
while line = Readline .readline ( " > " )
begin
puts eval (line) .inspect
rescue => e
puts e.to_s + " \ n " + e.backtrace.join ( " \ n " )
end
end


Execution Example:
 artem @ laptop: ~ / meta-lectures $ ruby ​​console.rb 
 > 1 + 2
 3
 > "hello"
 "hello"
 > def fib (n) (0..1) === n?  1: fib (n-1) + fib (n-2) end
 nil
 > (0 ... 10) .map {| n |  fib (n)}
 [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
 > 1/0
 (eval): 1: in `/ ': divided by 0
 console.rb: 4
 (eval): 1  
 > exit
 artem @ laptop: ~ / meta-lectures $

In scripting languages, there is an eval method that receives a string and executes this string in the current context as (almost so) as if it were written by a programmer at the place of the eval call.

Actually, the means similar to eval are in compiled programming languages.

By the way, I would not refer the eval method to metaprogramming and would even call it an extremely harmful method for this occupation. Interactive ruby ​​| python | perl | ...- shell - perhaps one of the few examples where it should be used. Let's talk about the harm of the eval method further.

attr_accessor



To determine the attributes of instances of a class, the attr_accessor construct attr_accessor used even by ruby ​​novices, it is not always true that this is a beast.

The meaning of the expression attr_accessor from the following statement: code
class Song
attr_accessor : title ,: length
end

equivalent to (by result) code
class Song
def title
@title
end
def title = (v)
@title = v
end
def length
@length
end
def length = (v)
@length = v
end
end

So much for the definition!

With the naked eye, it can be seen that attr_accessor is useful because it responds to the programmer’s internal desire to attr_accessor code short and intelligible. The attr_accessor construction can be translated as “I want set- and get-methods for the following attributes of class instances”.

The design of attr_accessor not even an unknown beast at all (read - it is not a built-in language construct), but an ordinary method that you can program yourself. Let's do this with the eval method.

def attr_accessor (* methods)
methods.each do | method |
eval % {
def # { method }
@ # { method }
end
def # { method } = (v)
@ # { method } = v
end
    
}
end
end

Now, attr_accessor , receiving as an argument an array of attribute names, for each name, executes lines of code that define the corresponding set and get methods.

The ability to write methods like attr_accessor appeared because Ruby does not have the concept of defining a class or method. Having written the line " class Song " we just moved to some new context in which you can do normal calculations, and the construction " def xxx() ... end " is just one of the expressions, the result of which is always nil (in ruby ​​v1.8 ), and the side effect is manifested in the emergence of the " xxx " method in the class in the context of which this construction was executed.

Function definition fulfilled? Switched to class context? What kind of nonsense? - it is not known from where the C ++ programmer, who came here, asks. Yes exactly.

A “ class Song ” does not frame the definition of a class in front, but makes a transition to a special context in which the namespace scope changes; that is, there are some new methods that we can call in this context, the meanings and effects of certain instructions change, etc. etc.

The text " def xxx() ... end " is really an expression and is executed by the Ruby virtual machine. In this case, the inside of the method definition is not executed, but the code is translated into byte and is stored under the method name.

Q: What does class context mean?


A: This is the context in which the expression self equals some class.

Run the following code:
puts " hi1 from # { self .inspect } "
class Abc
puts " hi2 from # { self .inspect } "
def hi
puts " hi3 from # { self .inspect } "
end
end

Lines with hi1 and hi2 will be printed. You will see the line with hi3 if you add
 Abc.new.hi

Total get:
 artem @ laptop: ~ / meta-lectures $ ruby ​​self_in_contexts.rb 
 hi1 from main
 hi2 from abc
 hi3 from # <Abc: 0xb7c3d9dc>
 artem @ laptop: ~ / meta-lectures $

You need to understand that when you write
 my_method (arg1, arg2)

then essentially the front is implicitly substituted with " self. ":
 self.my_method (arg1, arg2)


But these two expressions are not equivalent in some cases.
For example, when my_method is a private method, the expression self.my_method will fail to invoke the private method. These features of the Ruby implementation are private methods and there are some methods that cannot be called via a dot.


Okay, stop talking. attr_accessor above attr_accessor code to make it work:
class Module
def attr_accessor (* methods)
methods.each do | method |
class_eval % {
def # { method }
@ # { method }
end
def # { method } = (v)
@ # { method } = v
end
      
}
end
end
end

What have we done? We placed the method definition in the context of the Module class and replaced eval with class_eval .

Why did we do that? There are reasons:
* It is not good to write methods without understanding for what objects they will be available. We need to write a method attr_accessor , which can be used in the context of classes (instances of class Class ) and modules (instances of class Module ). The Class class inherits from the Module class, therefore it is sufficient to define this method as a method of Module instances, then it will be available for both modules and classes.
* The class_eval method has its differences from eval , in particular, the latter, when executing the expression " def ... end ", will create a definition of a method locally living inside the attr_accessor method and available only during the execution of the attr_accessor method (this is the undocumented def attr_accessor inside def ). The class_eval method executes the given code in the correct context, so that the " def " begin to produce the desired result. The class_eval method class_eval actively used in metaprogramming precisely in the variant where its argument is a block, not a string.

So now the code works. But he is wrong. There are other wrong decisions , including without the " class Module " and " class_eval ". Here is one of them:

def attr_accessor (* methods)
methods.each do | method |
eval % {
class # { self }
def # { method }
@ # { method }
end
def # { method } = (v)
@ # { method } = v
end
end
    
}
end
end


The last option is bad because you can call it not in the context of a class and get something bad, depending on what the self expression is in this context. For example:
s = " Class "
s.instance_eval { attr_accessor : hahaha }
Array .hahaha = 3 # in an unexpected way. The haray attribute of Array appeared.
puts Array .hahaha #


THE MOST IMPORTANT:
The described definitions of attr_assessor using eval are bad because they are not protected from the malicious intent of the enemy, nor from the stupidity of the programmer: if the value of the method variable is not a valid string for the method name, for example, the string " llalala(); puts `cat /etc/passwd`; puts ", the consequences will be unpredictable. You may not see any errors (exceptions) while executing the program; surprises will get only when “when the rocket will already fly” (c). There is nothing worse than errors that appear late, when the ends are no longer found.

Let's write, finally, the correct definition of attr_accessor . He, unlike the wrong, is unique:

class Module
def attr_accessor (* methods)
methods.each do | method |
raise TypeError .new ( " method name is not symbol " ) unless method.is_a? ( Symbol )
define_method (method) do
instance_variable_get ( " @ # { method } " )
end
define_method ( " # { method } = " ) do | v |
instance_variable_set ( " @ # { method } " , v)
end
end
end
end


attr_accessor with default value


We often write attributes with a default value. We do this using the idiom " ||= ", which roughly translates as "initialize what is left, what is right if it has not yet been initialized":

 class Song def length @length ||= 0 end def title @title ||= "no title" end end Song.new.length #=> 0 Song.new.title #=> "no title" 

after this definition, the value of the length attribute of the new song will be 0.

According to my wish, according to my wish ... let this code work the way I want !!!:
 class Song
   attr_accessor: length,: default => 0
   attr_accessor: title,: default => "no title"
 end

Let's write for learning purposes the wrong code using class_eval from the string:
class Module
def attr_accessor (* methods)
options = methods.last.is_a? ( Hash )? methods.pop: {}
methods.each do | method |
class_eval % {
def # { method }
\ # never write like that!
@ # { method }   # { " || = # { options [ : default ] } " if options [ : default ] }
end
def # { method } = (v)
@ # { method } = v
end
      
}
end
end
end
May there be a miracle !!!
 class Song
   attr_accessor: length,: default => 42
 end
 puts Song.new.length # prints 42 !!!


Wrong code also sometimes works. But this, of course, is no reason not to be fired by the programmer who writes it.

By doing
class Song
attr_accessor : length ,: default => 42
attr_accessor : title ,: default => " no title "
end
puts Song .new.length # prints 42 !!!
puts Song .new.title # oooooops !!!


we get mysterious:
 artem @ laptop: ~ / meta-lectures $ ruby ​​bad_attr_accessor.rb 
 42
 (eval): 5: in `title ': stack level too deep (SystemStackError)
	 from (eval): 5: in `title '
	 from bad_attr_accessor.rb: 27
 artem @ laptop: ~ / meta-lectures $ 

Why did such a nuisance arise? The fact is that there is a fundamental problem: inserting some objects into the line is simply impossible.

Correctly the problem of attr_accessor with the default value is solved as follows:

class Module
def attr_accessor (* methods)
options = methods.last.is_a? ( Hash )? methods.pop: {}
methods.each do | method |
raise TypeError .new ( " method name is not symbol " ) unless method.is_a? ( Symbol )
define_method (method) do
instance_variable_get ( " @ # { method } " ) ||
instance_variable_set ( " @ # { method } " , options [ : default ])
end
define_method ( " # { method } = " ) do | v |
instance_variable_set ( " @ # { method } " , v)
end
end
end
end


So, in the examples reviewed, metaprogramming looks like writing methods that define methods.

It makes sense for a beginner metaprogrammer to google such search queries:
1. ruby ​​doc attr_accessor
2. ruby ​​doc Kernel eval
3. ruby ​​doc Module class_eval
4. ruby ​​doc Object instance_eval
5. ruby ​​doc Object is_a?
The first links are correct.

How to metaprogram without eval , as well as about impurities, modifier methods, which allow to transfer abstraction tasks related to caching, RPC, DSL, patterns, continuing ideas of deferred (lazy) calculations, etc. to a new level. Read in future releases of the blog.

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


All Articles