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.
- A hozzászóláshoz be kell jelentkezni