Ruby Metaprogramming, Göründüğünden Daha Havalı
Yayınlanan: 2022-03-11Metaprogramlamanın yalnızca Ruby ninjalarının kullandığı bir şey olduğunu ve sıradan ölümlüler için olmadığını sık sık duyarsınız. Ama gerçek şu ki, metaprogramlama hiç de korkutucu bir şey değil. Bu blog yazısı, bu tür düşünceye meydan okumaya ve metaprogramlamayı ortalama Ruby geliştiricisine yaklaştırmaya hizmet edecek, böylece onların da faydalarından yararlanabilecekler.
Metaprogramlamanın çok şey ifade edebileceğini ve genellikle çok yanlış kullanılabileceğini ve kullanım söz konusu olduğunda aşırıya kaçabileceğini belirtmek gerekir, bu yüzden herkesin günlük programlamada kullanabileceği bazı gerçek dünyadan örnekler vermeye çalışacağım.
Metaprogramlama
Metaprogramlama, çalışma zamanında dinamik olarak kendi kendine kod yazan kod yazabileceğiniz bir tekniktir. Bu, çalışma zamanı sırasında yöntemleri ve sınıfları tanımlayabileceğiniz anlamına gelir. Çılgın, değil mi? Özetle, metaprogramlamayı kullanarak sınıfları yeniden açabilir ve değiştirebilir, var olmayan yöntemleri yakalayabilir ve bunları anında oluşturabilir, tekrarlardan kaçınarak DRY olan kodlar oluşturabilir ve daha fazlasını yapabilirsiniz.
Temeller
Ciddi metaprogramlamaya dalmadan önce temelleri araştırmalıyız. Ve bunu yapmanın en iyi yolu örnek olmaktır. Birinden başlayalım ve adım adım Ruby metaprogramlamasını anlayalım. Muhtemelen bu kodun ne yaptığını tahmin edebilirsiniz:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" end end
İki metod ile bir sınıf tanımladık. Bu sınıftaki ilk yöntem bir sınıf yöntemi, ikincisi ise bir örnek yöntemdir. Bu Ruby'deki temel şeylerdir, ancak bu kodun arkasında daha fazla ilerlemeden önce anlamamız gereken çok daha fazlası var. Developer
sınıfının kendisinin aslında bir nesne olduğunu belirtmekte fayda var. Ruby'de sınıflar dahil her şey bir nesnedir. Developer
bir örnek olduğundan, Class
sınıfının bir örneğidir. Ruby nesne modeli şu şekilde görünür:
p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject
Burada anlaşılması gereken önemli bir şey, self
anlamıdır. Ön frontend
yöntemi, Developer
sınıfının örneklerinde kullanılabilen normal bir yöntemdir, ancak backend
yöntemi neden bir sınıf yöntemidir? Ruby'de yürütülen her kod parçası belirli bir self'e karşı yürütülür. Ruby yorumlayıcısı herhangi bir kodu çalıştırdığında, verilen herhangi bir satır için her zaman self
değerini takip eder. self
her zaman bir nesneye atıfta bulunur, ancak bu nesne yürütülen koda göre değişebilir. Örneğin, bir sınıf tanımı içinde self
, Class
sınıfının bir örneği olan sınıfın kendisine atıfta bulunur.
class Developer p self end # Developer
Örnek yöntemlerinde self
, sınıfın bir örneğini ifade eder.
class Developer def frontend self end end p Developer.new.frontend # #<Developer:0x2c8a148>
Sınıf yöntemleri içinde self
, bir şekilde sınıfın kendisine atıfta bulunur (bu makalenin ilerleyen kısımlarında daha ayrıntılı olarak tartışılacaktır):
class Developer def self.backend self end end p Developer.backend # Developer
Bu iyi, ama sonuçta bir sınıf yöntemi nedir? Bu soruyu cevaplamadan önce, singleton class ve eigenclass olarak da bilinen metaclass denen bir şeyin varlığından bahsetmemiz gerekiyor. Daha önce tanımladığımız sınıf yöntemi frontend
ucu, Developer
nesnesi için metasınıfta tanımlanan bir örnek yönteminden başka bir şey değildir! Bir metasınıf, esasen, Ruby'nin sınıf yöntemlerini tutmak için kalıtım hiyerarşisine yarattığı ve eklediği, böylece sınıftan oluşturulan örneklere müdahale etmeyen bir sınıftır.
metasınıflar
Ruby'deki her nesnenin kendi metasınıfı vardır. Bir geliştirici için bir şekilde görünmezdir, ancak oradadır ve onu çok kolay bir şekilde kullanabilirsiniz. Developer
sınıfımız aslında bir nesne olduğundan, kendi metaclass'ına sahiptir. Örnek olarak, bir String
sınıfının nesnesini oluşturalım ve metasınıfını değiştirelim:
example = "I'm a string object" def example.something self.upcase end p example.something # I'M A STRING OBJECT
Burada yaptığımız şey, bir nesneye bir singleton yöntemi something
ekledik. Sınıf yöntemleri ile tekli yöntemler arasındaki fark, sınıf yöntemlerinin bir sınıf nesnesinin tüm örnekleri için kullanılabilirken tekil yöntemlerin yalnızca o tek örnek için mevcut olmasıdır. Sınıf yöntemleri yaygın olarak kullanılırken, tekil yöntemler çok fazla değildir, ancak her iki yöntem türü de o nesnenin bir meta sınıfına eklenir.
Önceki örnek şu şekilde yeniden yazılabilir:
example = "I'm a string object" class << example def something self.upcase end end
Sözdizimi farklıdır, ancak aynı şeyi etkili bir şekilde yapar. Şimdi Developer
sınıfını oluşturduğumuz önceki örneğe geri dönelim ve bir sınıf yöntemi tanımlamak için diğer bazı sözdizimlerini keşfedelim:
class Developer def self.backend "I am backend developer" end end
Bu, hemen hemen herkesin kullandığı temel bir tanımdır.
def Developer.backend "I am backend developer" end
Bu aynı şey, Developer
için backend
sınıf yöntemini tanımlıyoruz. self
kullanmadık ama böyle bir yöntemi etkili bir şekilde tanımlamak onu bir sınıf yöntemi yapar.
class Developer class << self def backend "I am backend developer" end end end
Yine, bir sınıf yöntemi tanımlıyoruz, ancak bir String
nesnesi için tek bir yöntem tanımlamak için kullandığımıza benzer bir sözdizimi kullanıyoruz. Burada bir Developer
nesnesinin kendisine atıfta bulunan self
kullandığımızı fark edebilirsiniz. İlk önce Developer
sınıfını açarak self'i Developer
sınıfına eşitledik. Daha sonra, self'i Developer
metasınıfına eşit yaparak class << self
yaparız. Ardından Developer
metaclass'ında bir method backend
tanımlarız.
class << Developer def backend "I am backend developer" end end
Böyle bir blok tanımlayarak, blok süresi boyunca self
Developer
metaclass'ına ayarlıyoruz. Sonuç olarak, sınıfın kendisi yerine backend
yöntemi Developer
meta sınıfına eklenir.
Bu metasınıfın kalıtım ağacında nasıl davrandığını görelim:
Önceki örneklerde gördüğünüz gibi, metasınıfın var olduğuna dair gerçek bir kanıt bile yok. Ancak bize bu görünmez sınıfın varlığını gösterebilecek küçük bir hack kullanabiliriz:
class Object def metaclass_example class << self self end end end
Object
sınıfında bir örnek yöntemi tanımlarsak (evet, herhangi bir sınıfı istediğimiz zaman yeniden açabiliriz, bu metaprogramlamanın başka bir güzelliği), içindeki Object
nesnesine atıfta bulunan bir self
olacaktır. Daha sonra, mevcut nesnenin metasınıfına işaret edecek şekilde mevcut nesneyi değiştirmek için class << self
sözdizimini kullanabiliriz. Geçerli nesne Object
sınıfının kendisi olduğundan, bu örneğin metasınıfı olacaktır. Yöntem, bu noktada bir meta sınıfın self
olan self'i döndürür. Böylece herhangi bir nesnede bu örnek yöntemini çağırarak o nesnenin bir metasınıfını elde edebiliriz. Developer
sınıfımızı tekrar tanımlayalım ve biraz keşfetmeye başlayalım:

class Developer def frontend p "inside instance method, self is: " + self.to_s end class << self def backend p "inside class method, self is: " + self.to_s end end end developer = Developer.new developer.frontend # "inside instance method, self is: #<Developer:0x2ced3b8>" Developer.backend # "inside class method, self is: Developer" p "inside metaclass, self is: " + developer.metaclass_example.to_s # "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"
Ve kreşendo için, ön ucun bir sınıfın örnek yöntemi ve backend
ucun bir frontend
örnek yöntemi olduğunun kanıtını görelim:
p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]
Bununla birlikte, metasınıfı almak için Object
yeniden açmanız ve bu hack'i eklemeniz gerekmez. Ruby'nin sağladığı singleton_class
kullanabilirsiniz. Eklediğimiz metaclass_example
ile aynı, ancak bu hack ile Ruby'nin kaputun altında nasıl çalıştığını gerçekten görebilirsiniz:
p developer.class.singleton_class.instance_methods false # [:backend]
“class_eval” ve “instance_eval” Kullanarak Yöntem Tanımlama
Bir sınıf yöntemi oluşturmanın bir yolu daha vardır ve bu, instance_eval
kullanmaktır:
class Developer end Developer.instance_eval do p "instance_eval - self is: " + self.to_s def backend p "inside a method self is: " + self.to_s end end # "instance_eval - self is: Developer" Developer.backend # "inside a method self is: Developer"
Bu kod parçası Ruby yorumlayıcısı, bu durumda bir Developer
nesnesi olan bir örnek bağlamında değerlendirir. Ve bir nesne üzerinde bir metot tanımlarken, ya bir sınıf metodu ya da bir singleton metodu yaratıyorsunuz. Bu durumda bu bir sınıf yöntemidir - kesin olmak gerekirse, sınıf yöntemleri tekil yöntemlerdir, ancak bir sınıfın tekli yöntemleridir, diğerleri ise bir nesnenin tekli yöntemleridir.
Öte yandan class_eval
, kodu bir örnek yerine bir sınıf bağlamında değerlendirir. Pratik olarak sınıfı yeniden açar. Bir örnek yöntemi oluşturmak için class_eval
nasıl kullanılabilir:
Developer.class_eval do p "class_eval - self is: " + self.to_s def frontend p "inside a method self is: " + self.to_s end end # "class_eval - self is: Developer" p developer = Developer.new # #<Developer:0x2c5d640> developer.frontend # "inside a method self is: #<Developer:0x2c5d640>"
Özetlemek gerekirse, class_eval
yöntemini çağırdığınızda, self'i orijinal sınıfa başvurmak için değiştirirsiniz ve instance_eval
öğesini çağırdığınızda, self
, orijinal sınıfın self
atıfta bulunmak için değişir.
Anında Eksik Yöntemleri Tanımlama
Metaprogramlama bulmacasının bir parçası daha method_missing
. Bir nesne üzerinde bir metot çağırdığınızda, Ruby önce sınıfa girer ve onun örnek metotlarına göz atar. Oradaki yöntemi bulamazsa atalar zincirini aramaya devam eder. Ruby yöntemi hala bulamazsa, her nesnenin miras aldığı bir Kernel
örnek yöntemi olan method_missing
adlı başka bir yöntemi çağırır. Ruby'nin eninde sonunda eksik metotlar için bu metodu çağıracağından emin olduğumuz için, bunu bazı hileleri uygulamak için kullanabiliriz.
define_method
, Module
sınıfında tanımlanmış, dinamik olarak metotlar oluşturmak için kullanabileceğiniz bir metottur. define_method
kullanmak için, onu yeni yöntemin adı ve bloğun parametrelerinin yeni yöntemin parametreleri haline geldiği bir blok ile çağırırsınız. Bir yöntem oluşturmak için def
kullanmak ile define_method
kullanmak arasındaki fark nedir? DRY kodu yazmak için method_missing
define_method
kullanabilmeniz dışında pek bir fark yoktur. Kesin olmak gerekirse, bir sınıfı tanımlarken kapsamları değiştirmek için def
yerine define_method
kullanabilirsiniz, ancak bu tamamen başka bir hikaye. Basit bir örneğe bakalım:
class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) end class << self def create_backend singleton_class.send(:define_method, "backend") do "Born from the ashes!" end end end end developer = Developer.new p developer.frontend(2, 5, 10) # => 100 p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError) Developer.create_backend p Developer.backend # "Born from the ashes!"
Bu, def
kullanmadan bir örnek yöntemi oluşturmak için define_method
nasıl kullanıldığını gösterir. Ancak, onlarla yapabileceğimiz daha çok şey var. Bu kod parçacığına bir göz atalım:
class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
Bu kod DRY değildir, ancak define_method
kullanarak DRY yapabiliriz:
class Developer ["frontend", "backend"].each do |method| define_method "coding_#{method}" do p "writing " + method.to_s end end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
Bu çok daha iyi, ama yine de mükemmel değil. Niye ya? Örneğin yeni bir metot coding_debug
eklemek istiyorsak, bu "debug"
ı diziye koymamız gerekiyor. Ancak method_missing
kullanarak bunu düzeltebiliriz:
class Developer def method_missing method, *args, &block return super method, *args, &block unless method.to_s =~ /^coding_\w+/ self.class.send(:define_method, method) do p "writing " + method.to_s.gsub(/^coding_/, '').to_s end self.send method, *args, &block end end developer = Developer.new developer.coding_frontend developer.coding_backend developer.coding_debug
Bu kod parçası biraz karmaşık, bu yüzden onu parçalayalım. Var olmayan bir yöntemin çağrılması, method_missing
. Burada sadece metod ismi "coding_"
ile başladığında yeni bir metod yaratmak istiyoruz. Aksi takdirde, aslında eksik olan bir yöntemi bildirme işini yapmak için süper çağırırız. Ve bu yeni yöntemi oluşturmak için sadece define_method
kullanıyoruz. Bu kadar! Bu kod parçasıyla, "coding_"
ile başlayan binlerce yeni yöntem oluşturabiliriz ve bu gerçek, kodumuzu DRY yapan şeydir. define_method
Module
için özel olduğu için, onu çağırmak için send
kullanmamız gerekiyor.
toparlamak
Bu sadece buzdağının görünen kısmı. Ruby Jedi olmak için başlangıç noktası burasıdır. Bu metaprogramlama yapı taşlarında ustalaştıktan ve özünü gerçekten anladıktan sonra, daha karmaşık bir şeye ilerleyebilirsiniz, örneğin kendi Etki Alanına Özgü Dilinizi (DSL) oluşturun. DSL başlı başına bir konudur ancak bu temel kavramlar ileri düzey konuları anlamak için bir ön koşuldur. Rails'de en çok kullanılan değerli taşlardan bazıları bu şekilde oluşturulmuştur ve muhtemelen RSpec ve ActiveRecord gibi DSL'sini bilmeden kullanmışsınızdır.
Umarım bu makale sizi metaprogramlamayı anlamaya ve hatta daha verimli kodlamak için kullanabileceğiniz kendi DSL'nizi oluşturmaya bir adım daha yaklaştırabilir.