Ruby Metaprogrammeren is nog Cooler dan het klinkt

Je hoort vaak dat metaprogrammeren iets is dat alleen Ruby ninja’s gebruiken, en dat het gewoon niet voor gewone stervelingen is. Maar de waarheid is dat metaprogrammeren helemaal niet iets engs is. Deze blog post zal dienen om deze manier van denken uit te dagen en om metaprogramming dichter bij de gemiddelde Ruby ontwikkelaar te brengen, zodat ook zij er de vruchten van kunnen plukken.

Ruby Metaprogramming: Code Writing Code

Het moet worden opgemerkt dat metaprogrammeren veel kan betekenen en het kan vaak erg verkeerd worden gebruikt en tot het uiterste gaan als het gaat om het gebruik, dus ik zal proberen er een paar voorbeelden uit de echte wereld in te gooien die iedereen zou kunnen gebruiken in het dagelijks programmeren.

Metaprogrammeren

Metaprogrammeren is een techniek waarmee je code kunt schrijven die zelf dynamisch code schrijft tijdens runtime. Dit betekent dat je methoden en klassen kunt definiëren tijdens runtime. Gek, toch? In een notendop, met behulp van metaprogramming kun je klassen heropenen en wijzigen, methoden vangen die niet bestaan en ze on the fly maken, code maken die DRY is door herhalingen te vermijden, en nog veel meer.

De basis

Voordat we in het serieuze metaprogramming duiken, moeten we de basis verkennen. En de beste manier om dat te doen is met voorbeelden. Laten we beginnen met een voorbeeld en Ruby metaprogrammeren stap voor stap begrijpen. Je kunt waarschijnlijk wel raden wat deze code doet:

class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" endend

We hebben een klasse gedefinieerd met twee methoden. De eerste methode in deze klasse is een klasse methode en de tweede is een instantie methode. Dit is basis spul in Ruby, maar er gebeurt veel meer achter deze code dat we moeten begrijpen voordat we verder gaan. Het is de moeite waard om erop te wijzen dat de klasse Developer zelf eigenlijk een object is. In Ruby is alles een object, inclusief klassen. Omdat Developer een instantie is, is het een instantie van de klasse Class. Here is how the Ruby object model looks like:

p Developer.class # Classp Class.superclass # Modulep Module.superclass # Objectp Object.superclass # BasicObject

One important thing to understand here is the meaning of self. The frontend method is a regular method that is available on instances of class Developer, but why is backend method a class method? Every piece of code executed in Ruby is executed against a particular self. When the Ruby interpreter executes any code it always keeps track of the value self for any given line. self is always referring to some object but that object can change based on the code executed. For example, inside a class definition, the self refers to the class itself which is an instance of class Class.

class Developer p self end# Developer

Inside instance methods, self refers to an instance of the class.

class Developer def frontend self endend p Developer.new.frontend# #<Developer:0x2c8a148>

In class methods, self verwijst op een bepaalde manier naar de class zelf (die later in dit artikel uitgebreider aan de orde komt):

class Developer def self.backend self endendp Developer.backend# Developer

Dit is prima, maar wat is een class method nu eigenlijk? Alvorens die vraag te beantwoorden moeten we eerst wijzen op het bestaan van iets dat metaclass heet, ook bekend als singleton class en eigenclass. Klassenmethode frontend die we eerder definieerden is niets anders dan een instance methode gedefinieerd in de metaclass voor het object Developer! Een metaclass is in wezen een klasse die Ruby creëert en in de overervings-hiërarchie invoegt om methoden van de klasse te bevatten, en zo niet te interfereren met instanties die vanuit de klasse worden gecreëerd.

Metaclasses

Elk object in Ruby heeft zijn eigen metaclass. Het is op de een of andere manier onzichtbaar voor een ontwikkelaar, maar het is er en je kunt het heel gemakkelijk gebruiken. Omdat onze klasse Developer in essentie een object is, heeft het zijn eigen metaclass. Laten we als voorbeeld een object van een klasse String maken en zijn metaclass manipuleren:

example = "I'm a string object"def example.something self.upcaseendp example.something# I'M A STRING OBJECT

Wat we hier gedaan hebben is dat we een singleton methode something aan een object hebben toegevoegd. Het verschil tussen klasse methodes en singleton methodes is dat klasse methodes beschikbaar zijn voor alle instanties van een klasse object terwijl singleton methodes alleen beschikbaar zijn voor die ene instantie. Klasse methoden worden veel gebruikt en singleton methoden niet zo veel, maar beide soorten methoden worden toegevoegd aan een metaklasse van dat object.

Het vorige voorbeeld zou als volgt herschreven kunnen worden:

example = "I'm a string object"class << example def something self.upcase endend

De syntax is anders maar het doet in feite hetzelfde. Laten we nu teruggaan naar het vorige voorbeeld waar we Developer class hebben gemaakt en enkele andere syntaxen verkennen om een class methode te definiëren:

class Developer def self.backend "I am backend developer" endend

Dit is een basisdefinitie die bijna iedereen gebruikt.

def Developer.backend "I am backend developer"end

This is the same thing, we are defining the backend class method for Developer. We didn’t use self but defining a method like this effectively makes it a class method.

class Developer class << self def backend "I am backend developer" end endend

Again, we are defining a class method, but using syntax similar to one we used to define a singleton method for a String object. You may notice that we used self here which refers to a Developer object itself. First we opened Developer class, making self equal to the Developer class. Next, we do class << self, making self equal to Developer’s metaclass. Then we define a method backend on Developer’s metaclass.

class << Developer def backend "I am backend developer" endend

By defining a block like this, we are setting self to Developer’s metaclass for the duration of the block. As a result, the backend method is added to Developer’s metaclass, rather than the class itself.

Let’s see how this metaclass behaves in the inheritance tree:

As you saw in previous examples, there’s no real proof that metaclass even exists. But we can use a little hack that can show us the existence of this invisible class:

class Object def metaclass_example class << self self end endend

If we define an instance method in Object class (yes, we can reopen any class anytime, that’s yet another beauty of metaprogramming), we will have a self referring to the Object object inside it. We kunnen dan de class << self syntax gebruiken om de huidige zelf te veranderen om naar de metaklasse van het huidige object te verwijzen. Aangezien het huidige object zelf Object klasse is, zou dit de metaklasse van de instantie zijn. De methode retourneert self dat op dit punt zelf een metaklasse is. Dus door deze instance methode op een object aan te roepen kunnen we een metaclass van dat object krijgen. Laten we onze Developer klasse opnieuw definiëren en wat gaan verkennen:

En voor het crescendo, laten we het bewijs zien dat frontend een instantie methode van een klasse is en backend een instantie methode van een metaklasse is:

Hoewel, om de metaklasse te krijgen hoeft u Object niet daadwerkelijk te heropenen en deze hack toe te voegen. U kunt singleton_class gebruiken dat Ruby biedt. Het is hetzelfde als metaclass_example dat we hebben toegevoegd, maar met deze hack kunt u zien hoe Ruby onder de motorkap werkt:

p developer.class.singleton_class.instance_methods false# 

Methoden definiëren met “class_eval” en “instance_eval”

Er is nog een manier om een methode voor een klasse te maken, en dat is door instance_eval te gebruiken:

Dit stukje code evalueert Ruby interpreter in de context van een instantie, die in dit geval een Developer object is. En wanneer je een methode op een object definieert, creëer je ofwel een klassemethode of een singleton methode. In dit geval is het een klassemethode – om precies te zijn, klassemethoden zijn singleton methoden, maar singleton methoden van een klasse, terwijl de andere singleton methoden van een object zijn.

Aan de andere kant, class_eval evalueert de code in de context van een klasse in plaats van een instantie. Het heropent praktisch de klasse. Hier is hoe class_eval kan worden gebruikt om een instantie-methode te maken:

Om samen te vatten, wanneer je class_eval methode aanroept, verander je self om naar de oorspronkelijke klasse te verwijzen en wanneer je instance_eval aanroept, verandert self om naar de metaklasse van de oorspronkelijke klasse te verwijzen.

Missing Methods on the Fly definiëren

Een ander stukje van de metaprogrammeer-puzzel is method_missing. Wanneer je een methode op een object aanroept, gaat Ruby eerst in de klasse en doorzoekt de instantie methoden. Als het de methode daar niet vindt, gaat het verder zoeken in de voorouder keten. Als Ruby de methode nog steeds niet vindt, roept het een andere methode aan genaamd method_missing dat is een instance methode van Kernel dat elk object erft. Omdat we zeker weten dat Ruby deze methode uiteindelijk gaat aanroepen voor ontbrekende methoden, kunnen we dit gebruiken om enkele trucs te implementeren.

define_method is een methode die is gedefinieerd in de Module klasse die je kunt gebruiken om methoden dynamisch te maken. Om define_method te gebruiken, roep je het op met de naam van de nieuwe methode en een blok waar de parameters van het blok de parameters van de nieuwe methode worden. Wat is het verschil tussen het gebruik van def om een methode te maken en define_method? Er is niet veel verschil, behalve dat je define_method kunt gebruiken in combinatie met method_missing om DRY code te schrijven. Om precies te zijn, u kunt define_method gebruiken in plaats van def om scopes te manipuleren bij het definiëren van een klasse, maar dat is een heel ander verhaal. Laten we eens kijken naar een eenvoudig voorbeeld:

Dit laat zien hoe define_method is gebruikt om een instantie-methode te maken zonder een def te gebruiken. Er is echter veel meer dat we met hen kunnen doen. Laten we eens kijken naar dit stukje code:

class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" endenddeveloper = Developer.newdeveloper.coding_frontend# "writing frontend"developer.coding_backend# "writing backend"

Deze code is niet DRY, maar met define_method kunnen we hem DRY maken:

Dat is al veel beter, maar nog steeds niet perfect. Waarom? Als we bijvoorbeeld een nieuwe methode coding_debug willen toevoegen, dan moeten we deze "debug" in de array zetten. Maar met method_missing kunnen we dit oplossen:

Dit stukje code is een beetje ingewikkeld, dus laten we het even uit elkaar halen. Het aanroepen van een methode die niet bestaat zal method_missing doen opstarten. Hier willen we alleen een nieuwe methode maken als de naam van de methode begint met "coding_". Anders roepen we gewoon super aan om het werk te doen van het melden van een methode die eigenlijk ontbreekt. En we gebruiken gewoon define_method om die nieuwe methode te maken. Dat is het! Met dit stukje code kunnen we letterlijk duizenden nieuwe methodes maken die beginnen met "coding_", en dat feit is wat onze code DRY maakt. Omdat define_method toevallig privé is voor Module, moeten we send gebruiken om het aan te roepen.

Wrapping up

Dit is slechts het topje van de ijsberg. Om een Ruby Jedi te worden, is dit het beginpunt. Nadat je deze bouwstenen van metaprogrammeren onder de knie hebt en de essentie ervan echt begrijpt, kun je verder gaan met iets complexers, bijvoorbeeld je eigen Domain-specific Language (DSL) maken. DSL is een onderwerp op zich, maar deze basisconcepten zijn een eerste vereiste om geavanceerde onderwerpen te begrijpen. Sommige van de meest gebruikte juweeltjes in Rails zijn op deze manier gebouwd en je hebt waarschijnlijk de DSL ervan gebruikt zonder dat je het wist, zoals RSpec en ActiveRecord.

Hopelijk kan dit artikel je een stap dichter brengen bij het begrijpen van metaprogramming en misschien zelfs het bouwen van je eigen DSL, die je kunt gebruiken om efficiënter te coderen.

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.