Ruby Metaprogramming, Göründüğünden Daha Havalı

Yayınlanan: 2022-03-11

Metaprogramlamanı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.

Ruby Metaprogramming: Kod Yazma Kod
Cıvıldamak

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:

Ruby nesne modeli

 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:

Kalıtım ağacında metasınıf

Ö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.