1.4.1.20. fejezet, Dinamikus programozás avagy hogyan változtassunk a programon futás közben

Önmódosító programok

A lehetőséget, hogy az adatot, mint futtatható kódot kezelni tudja egy program, meta-programozásnak nevezik. Már rengetegszer csináltunk meta-programozást, mikor kifejezéseket írtunk stringekbe a #{} jelek közé. De nem egyszerű kifejezésekkel is feltölthető ez a string kiértékelés:

"#{def x(s)
     puts(s.reverse)
   end;
(1..3).each{x(aStr)}}"

Egész programokat idézőjelek közé írni értelmetlen erőfeszítés lenne, habár elég sok alkalommal elegendő ezt a módszert alkalmazni.

A meta-programozást használhatjuk arra is, hogy megismerkedjünk a mesterséges intelligencia és a 'gépi tanulás' működésével. A meta-programozás mindenütt előfordul a Ruby-ban. Emlékezzünk csak vissza, mikor egy osztály tulajdonságainak író/olvasó metódusokat definiáltunk egyszerűen az attr_accessor-al, és létrejött két metódus.

Az eval varázslat

Az eval metódus egyszerű lehetőséget ad egy stringben leírt kifejezés kiértékeléséhet. Hasonló működésben a #{} kifejezés kiértékelésében. Az alábbinak ugyan az az eredménye.

puts( eval("1 + 2" ) )
puts( "#{1 + 2}" )

Lehet mégis olyan szituáció, amikor az eredmények különbözők lesznek. Pl.:

exp = gets().chomp()
puts( eval( exp )) 
puts( "#{exp}" )

Legyen 2 * 4, amit beírunk a gets metódussal, és ez az exp változóba kerül. Mikor kiértékeljük az eval metódussal, 8-at kapunk eredményül. De ha kiértékeljük mint string kifejezést a #{} jelek közt, az eredmény "2*4". Ez azért van, mert a bekért érték, amit a gets visszaad egy string objektum, a #{}, és stringként értékeli, nem kifejezésként, míg az eval a stringet kifejezésként értékeli ki. Ha rákényszerülünk a stringbe való kiértékelésre, betehetünk egy eval kiértékelést a stringbe:

puts( "#{eval(exp)}" )

Nézzük a következő példát.

print( "Enter the name of a string method (e.g. reverse or upcase): " ) # user enters: upcase 
methodname = gets().chomp() 
exp2 = "'Hello world'."<< methodname
puts( eval( exp2 ) ) #=> HELLO WORLD
puts( "#{exp2}" ) #=> „Hello world‟.upcase
puts( "#{eval(exp2)}" ) #=> HELLO WORLD

Az eval több sor programrészt ki tud értékelni, lehetővé téve, hogy egész programok fussanak le egy stringet kiértékelve.

input = "" 
until input == "q"
  input = gets().chomp() 
  if input != "q" then 
    eval( input )
  end 
end

Ez már egészen úgy néz ki, mint egy program, amit a felhasználó gépel be a Ruby alkalmazásba. Írjuk be a következőket:

def x(aStr); puts(aStr.upcase);end
def y(aStr); puts(aStr.reverse);end

Figyeljük meg, hogy minden egyes metódus egy sorban szerepel, ahogy begépeljük. Ez azért van, mert a fenti program soronként értékeli ki a bevitt stringeket. Később megnézzük hogyan kerülhető el ez a korlátozás. Az eval-nak köszönhetően mindkét metódus valóban létrejött, és működő Ruby kód. Próbáljuk is ki:

x("hello world") 
y("hello world")

Eredménye pedig a következő:

HELLO WORLD
dlrow olleh

Nem rossz! Csak néhány sor program, és működik.

Az eval speciális típusai

Az eval-nak hatáskörtől függően több változata van. Ilyen az instance_eval, module_eval és class_eval. Az instance_eval egy objektumból hívható, és eléri az objektum tulajdonságait. Hívható egy blokkal, vagy egy string-el.

class MyClass
 def initialize
   @aVar = "Hello world"
 end
end
 
ob = MyClass.new
p( ob.instance_eval { @aVar } )			#=> "Hello world"
p( ob.instance_eval( "@aVar" ) )		#=> "Hello world"
# p( ob.eval( "@aVar" )  )			#=> error: eval is a private method
 
# class Object
    public :eval	#=> Try commenting this out!
# end
 
p( ob.eval( "@aVar" ) )	#=> "Hello world"

Miután az Object osztály privát metódusa az eval, az instance_eval-t kell használni. A láthatóság, mint a fenti kód mutatja, változtatható, és az eval kívülről elérhetővé válhat. Megjegyzem, írhattunk volna egyszerűen public :eval kifejezést, hiszen az aktuális hatáskörön belül működik a parancs, ami jelenleg az Object osztály, így ezt nem kötelező meghatározni.

Ugyan így máködik a module_eval és class_eval.

module X
end
 
class Y
	@@x = 10
	include X
end
 
X::module_eval{ define_method(:xyz){ puts("hello" ) } }
Y::class_eval{ define_method(:abc){ puts("hello, hello" ) } }
X::class_eval{ define_method(:xyz2){ puts("hello again" ) } }
Y::module_eval{ define_method(:abc2){ puts("hello, hello again" ) } }
String::class_eval{ define_method(:bye){ puts("goodbye" ) } }
 
 
ob = Y.new
ob.xyz
ob.abc
ob.xyz2
ob.abc2
"Hello".bye
p( Y.class_eval( "@@x" ) )		

Eredmények:

hello
hello, hello
hello again
hello, hello again
goodbye
10

Változók és metódusok hozzáadása

A module_eval és a class_eval alkalmazható az osztály változók és tulajdonságok megtekintésére. Tartsuk észben, hogy minél többször alkalmazzuk ezeket a metódusokat, annál jobban sértjük az egységbe zárás szabályát. Pl.:

Y.class_eval( "@@x" )

A class_eval metódus összetett kiértékelésekre képes. Például egy új metódust adhatunk az osztályhoz.

ob = X.new
X.class_eval( 'def hi;puts("hello");end' ) 
ob.hi #=> “hello”

Most térjünk vissza az osztály változó műveletekhez. A class_variable_get egy szimbólum paraméter segítségével visszaadja az osztály-változó értékét. A class_variable_set pedig a szimbólum mellett kap egy második paramétert, ami az osztály-változó értékére vonatkozik. Íme a mintaprogram:

class X
  @@abc = 100	
 
  def self.addvar( aSymbol, aValue )
    class_variable_set( aSymbol, aValue )
  end
 
  def self.getvar( aSymbol )
    return class_variable_get( aSymbol )
  end
end
 
puts( X.class_eval( '@@abc' ) )
X.class_eval( '@@abc=500' )
puts( X.class_eval( '@@abc' ) )
 
ob = X.new
X.class_eval( 'def hi;puts("hello");end' )
ob.hi
 
X.addvar( :@@newvar, 2000 )
puts( X.getvar( :@@newvar ) )
 
p( X.class_variables )

Eredménye:

100
500
hello
2000
[:@@abc, :@@newvar]

Egy osztály-változó tömböt kapunk vissza a class_variables metódus meghívásával.

p( X.class_variables ) #=> ["@@abc", "@@newvar"]

Objektum tulajdonságokat kérdezhetünk le és állíthatunk be az instance_variable_get("@tulajdonság") és instance_variable_set("@tulajdonság", érték) metódusokkal.

ob = X.new
ob.instance_variable_set("@aname", "Bert")

Ezzel, és a metódus hozzáadással egy arcátlan programozó (vagy éppenséggel egy merész), teljesen átírhatja az objektum belsejét, kívülről. A következő mintaprogram az X osztály addMethod metódusával küld üzenetet az objektumnak. A send első paramétere a metódus szimbóluma, aminek átadja az utána következő argumentumokat.

class X
  def a
    puts("method a")
  end
 
  def addMethod( m, &block )
    self.class.send( :define_method, m , &block )
  end
end
 
ob = X.new
ob.instance_variable_set("@aname", "Bert")
ob.addMethod( :xyz ) { puts("My name is #{@aname}") } 
ob.xyz
 
ob2 = X.new
ob2.instance_variable_set("@aname", "Mary")
ob2.xyz
puts( ob2.instance_variable_get( :@aname ) )
 
X::const_set( :NUM, 500 )
puts( X::const_get( :NUM ) )

Eredménye:

My name is Bert
My name is Mary
Mary
500

Az utolsó két sor egy konstans létrehozását és lekérdezését mutatja be. Ezzel egyben az osztály neve is lekérdezhető, és így új példány hozható létre belőle.

class X
  def y
    puts( "ymethod" )
  end
end
 
print( "Enter a class name: ")
cname = gets().chomp
ob = Object.const_get(cname).new
p( ob )
print( "Enter a method to be called: " )
mname = gets().chomp
ob.method(mname).call

Eredménye:

Enter a class name: X
#<X:0x9766c3c>
Enter a method to be called: y
ymethod

Osztályok létrehozása futás közben

Amíg a const_get az osztálynév eléréséhez alkalmazható, a const_set osztály létrehozására. A következő példa az osztálynév bekérésére, metódus hozzáadására (myname), példány létrehozására (x), majd a létrehozott metódus meghívására.

puts("What shall we call this class? ")
className = gets.strip().capitalize()		# make sure class name starts with capital letter
Object.const_set(className,Class.new)		# create new class
puts("I'll give it a method called 'myname'" )
className = Object.const_get(className)
className::module_eval{ define_method(:myname){ 
  puts("The name of my class is '#{self.class}'" ) } 
}
x = className.new
x.myname

Kötések

Az eval metódus második argumentumaként kaphat egy kötődési objektumot, ami meghatározza a kiértékelés hatáskörét. A kötés egy példánya a Binding osztálynak. Létrehozható a binding metódus meghívásával.

def getBinding(str)
  return binding()
end
str = "hello"
puts( eval( "str + ' Fred'" )   )                  #=> "hello Fred"
puts( eval( "str + ' Fred'", getBinding("bye") ) ) #=> "bye Fred"

Itt a binding privát metódusa a Kernel osztálynak. Eléri a binding metódust az aktuális környezetbe, és visszatér az str értékével. Az eval első hívásakor a környezet a main objektum, és a lokális változó értékét használja fel a puts ('hello'). A második hívásnál a getBinding metódus a környezet, és az argumentumul kapott str értékét használja fel, azaz a 'bye' értékét állítja be str értékeként, és így értékeli ki a kifejezést.

Környezet definiálható egy osztállyal. A következő példa az @mystr tulajdonság és a @@x osztály-változó viselkedését mutatja be.

class MyClass
  @@x = " x"
  def initialize(s)
    @mystr = s
  end
 
  def getBinding
    return binding()
  end
end
 
class MyOtherClass 
  @@x = " y"
  def initialize(s)
    @mystr = s
  end
 
  def getBinding
    return binding()
  end
end
 
@mystr = self.inspect
@@x = " some other value"
 
ob1 = MyClass.new("ob1 string")
ob2 = MyClass.new("ob2 string")
ob3 = MyOtherClass.new("ob3 string")
 
puts(eval("@mystr << @@x", ob1.getBinding))
puts(eval("@mystr << @@x", ob2.getBinding))
puts(eval("@mystr << @@x", ob3.getBinding))
puts(eval("@mystr << @@x", binding))

Eredmények 1.8-as Ruby-ban:

ob1 string x
ob2 string x
ob3 string y
main some other value

Eredmények 1.9-es Ruby-ban:

ob1 string some other value
ob2 string some other value
ob3 string some other value
main some other value

Változtassuk most meg a globális @@x definíciójának helyét, és nézzük meg 1.9-es Rubyval újra az eredményét:

@mystr = self.inspect
 
ob1 = MyClass.new("ob1 string")
ob2 = MyClass.new("ob2 string")
ob3 = MyOtherClass.new("ob3 string")
 
 
puts(eval("@mystr << @@x", ob1.getBinding))
puts(eval("@mystr << @@x", ob2.getBinding))
puts(eval("@mystr << @@x", ob3.getBinding))
 
@@x = " some other value"
 
puts(eval("@mystr << @@x", binding))

Eredményül ugyan azt kapjuk, mint az 1.8-ban. Valami itt hiányzik a dokumentációból, mert ezt a változást nem említi (TODO).

Üzenet küldés (send)

Használhatjuk a send metódust annak a metódusnak a meghívására, amit paraméterül adunk meg neki, pl.:

name = "Fred"
puts( name.send( :reverse ) ) #=> derF
puts( name.send( :upcase ) ) #=> FRED

A dokumentációban az áll, hogy szimbólumot vár paraméterül, de adhatunk stringe-et is, vagy használhatjuk a to_sym metódust.

name = MyString.new( gets() ) 
methodname = gets().chomp.to_sym #<= to_sym is not strictly necessary
name.send(methodname)

Itt még egy példa, hogyan hívható meg egy metódus, beolvasással megadott string-et használva:

class MyString < String
  def initialize( aStr )
    super aStr
  end
 
  def show
    puts self
  end
 
  def rev
    puts self.reverse
  end
end
 
print("Enter your name: ")		#<= Enter: Fred
name = MyString.new( gets() )
print("Enter a method name: " )		#<= Enter: rev (or show)
methodname = gets().chomp.to_sym
puts( name.send(methodname) )

Metódus eltávolítása

Néha előfordul, hogy metódus létrehozása helyett annak eltávolítása a célunk. Használjuk a remove_method metódust erre az adott osztály hatáskörében.

puts( "hello".reverse ) 
class String
  remove_method( :reverse ) 
end
puts( "hello".reverse ) #=> „undefined method‟ error!

Ha egy metódus definiálva van ugyan azzal a névvel az ősosztályban, azt nem távolítja el. Használjuk az undef_method metódust erre a célra.

Hiányzó metódusok kezelése

Ha egy metódust hívnánk, ami nincsen definiálva az osztályba, akkor kivétel keletkezik, és ezt kezelnünk illik. Ha nem kívánunk ilyen módon reagálni a hiányzó metódus hívására, definiáljuk az osztályban a method_missing metódust, és ez fog hívódni ilyen esetben, és írjuk ki a hiány tényét.

def method_missing( methodname ) 
  puts( "#{methodname} does not exist" ) 
end
 
xxx #=> displays: „xxx does not exist‟

A method_missing metódus képes argumentumtömböt is fogadni.

def method_missing( methodname, *args ) 
  puts( "Class #{self.class} does not understand: #{methodname}( #{args.inspect} )" ) 
end

Vagy képessé válhat a programunk definiálni a hiányzó metódust:

class X
  def method_missing( methodname, *args )
    puts( "Class #{self.class} does not understand: #{methodname}( #{args.inspect} )" )
  end	
end
 
class Y < X	
  def aaa
    puts( "aaa method" )
  end	
  remove_method( :aaa )
end
 
class Z < Y
  def method_missing( methodname, *args )
    super
    puts( "Now creating method #{methodname}( )" )
    self.class.send( :define_method, methodname, lambda{ |*args| puts( args.inspect) } )
  end	
end
 
ob = X.new
ob.aaa( 1,2,3 )
ob2 = Y.new
ob2.ccc( "hello world" )
ob3 = Z.new
ob3.ddd( 1,2,3)
ob3.ddd( 4,5,6 )

Eredménye:

Class X does not understand: aaa( [1, 2, 3] )
Class Y does not understand: ccc( ["hello world"] )
Class Z does not understand: ddd( [1, 2, 3] )
Now creating method ddd( )
[4, 5, 6]

Objektumok lefagyasztása

Ha úgy gondoljuk, készen van az osztály, és nem szeretnénk, hogy interaktívan módosítani lehessen a belőle létrehozott objektumot, fagyasszuk le a freeze metódussal. Ha egy lefagyasztott objektum módosítását kezdeményeznénk, TypeError kivétel keletkezik. Azonban ha egyszer mirelittet csináltunk már egy objektumunkból, sajnos nem lehet kiolvasztani.

s = "Hello"
s << " world"
s.freeze
 s << " !!!" # Error: "can't modify frozen string (TypeError)"

Egy objektumról tudni szeretnénk, hogy lefagyasztottuk-e már, használhatjuk a frozen? metódusát. De lefagyszthatjuk az osztályt is.

X.freeze
if not( X.frozen? )
  then ob.addMethod( :def ) { puts("'def' is not a good name for a method") }
end

Ne feledjük: az osztály is egy objektum a Ruby világában.