La metaprogrammazione in Ruby è ancora più bella di quanto sembri

Si sente spesso dire che la metaprogrammazione è qualcosa che solo i ninja di Ruby usano, e che semplicemente non è per i comuni mortali. Ma la verità è che la metaprogrammazione non è affatto qualcosa di spaventoso. Questo post del blog servirà a sfidare questo tipo di pensiero e ad avvicinare la metaprogrammazione allo sviluppatore Ruby medio, in modo che anche loro possano trarne beneficio.

Metaprogrammazione Ruby: Codice che scrive codice

Si deve notare che la metaprogrammazione può significare molto e spesso può essere usata in modo improprio e andare all’estremo quando si tratta di utilizzo, quindi cercherò di inserire alcuni esempi del mondo reale che tutti potrebbero usare nella programmazione quotidiana.

Metaprogrammazione

La metaprogrammazione è una tecnica con cui si può scrivere codice che scrive codice da solo dinamicamente in fase di esecuzione. Questo significa che si possono definire metodi e classi durante il runtime. Pazzesco, vero? In poche parole, usando la metaprogrammazione puoi riaprire e modificare classi, catturare metodi che non esistono e crearli al volo, creare codice che è DRY evitando ripetizioni, e altro ancora.

Le basi

Prima di tuffarci nella metaprogrammazione seria dobbiamo esplorare le basi. E il modo migliore per farlo è l’esempio. Cominciamo con uno di essi per capire la metaprogrammazione di Ruby passo dopo passo. Probabilmente potete indovinare cosa sta facendo questo codice:

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

Abbiamo definito una classe con due metodi. Il primo metodo di questa classe è un metodo di classe e il secondo è un metodo di istanza. Questa è roba di base in Ruby, ma c’è molto di più dietro questo codice che dobbiamo capire prima di procedere oltre. Vale la pena sottolineare che la classe Developer stessa è in realtà un oggetto. In Ruby tutto è un oggetto, comprese le classi. Poiché Developer è un’istanza, è un’istanza della classe 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>

Nei metodi di classe, self si riferisce alla classe stessa in un modo (che sarà discusso in dettaglio più avanti in questo articolo):

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

Questo va bene, ma cos’è un metodo di classe dopo tutto? Prima di rispondere a questa domanda dobbiamo menzionare l’esistenza di qualcosa chiamato metaclasse, noto anche come classe singleton e eigenclass. Il metodo di classe frontend che abbiamo definito prima non è altro che un metodo di istanza definito nella metaclasse per l’oggetto Developer! Una metaclasse è essenzialmente una classe che Ruby crea e inserisce nella gerarchia dell’ereditarietà per contenere i metodi della classe, non interferendo così con le istanze che vengono create dalla classe.

Metaclassi

Ogni oggetto in Ruby ha la sua metaclasse. È in qualche modo invisibile per uno sviluppatore, ma è lì e si può usare molto facilmente. Poiché la nostra classe Developer è essenzialmente un oggetto, ha la sua metaclasse. Come esempio, creiamo un oggetto di una classe String e manipoliamo la sua metaclasse:

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

Quello che abbiamo fatto qui è aggiungere un metodo singleton something ad un oggetto. La differenza tra i metodi di classe e i metodi singleton è che i metodi di classe sono disponibili a tutte le istanze di un oggetto di classe mentre i metodi singleton sono disponibili solo a quella singola istanza. I metodi di classe sono ampiamente usati mentre i metodi singleton non tanto, ma entrambi i tipi di metodi sono aggiunti ad una metaclasse di quell’oggetto.

L’esempio precedente potrebbe essere riscritto così:

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

La sintassi è diversa ma fa effettivamente la stessa cosa. Ora torniamo all’esempio precedente dove abbiamo creato la classe Developer ed esploriamo alcune altre sintassi per definire un metodo di classe:

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

Questa è una definizione di base che quasi tutti usano.

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. Possiamo quindi usare la sintassi class << self per cambiare il self corrente per puntare alla metaclasse dell’oggetto corrente. Poiché l’oggetto corrente è la classe Object stessa, questa sarebbe la metaclasse dell’istanza. Il metodo restituisce self che è a questo punto una metaclasse stessa. Quindi chiamando questo metodo di istanza su qualsiasi oggetto possiamo ottenere una metaclasse di quell’oggetto. Definiamo di nuovo la nostra classe Developer e cominciamo ad esplorare un po’:

E per il crescendo, vediamo la prova che frontend è un metodo di istanza di una classe e backend è un metodo di istanza di una metaclasse:

Anche se, per ottenere la metaclasse non avete bisogno di riaprire effettivamente Object e aggiungere questo hack. Puoi usare singleton_class che Ruby fornisce. È lo stesso di metaclass_example che abbiamo aggiunto, ma con questo hack potete effettivamente vedere come Ruby funziona sotto il cofano:

p developer.class.singleton_class.instance_methods false# 

Definire i metodi usando “class_eval” e “instance_eval”

C’è un altro modo per creare un metodo di classe, ed è usando instance_eval:

Questo pezzo di codice che l’interprete Ruby valuta nel contesto di un’istanza, che in questo caso è un oggetto Developer. E quando si definisce un metodo su un oggetto si sta creando un metodo di classe o un metodo singleton. In questo caso è un metodo di classe – per essere precisi, i metodi di classe sono metodi singleton ma metodi singleton di una classe, mentre gli altri sono metodi singleton di un oggetto.

D’altra parte, class_eval valuta il codice nel contesto di una classe invece di un’istanza. Praticamente riapre la classe. Ecco come class_eval può essere usato per creare un metodo di istanza:

Per riassumere, quando si chiama il metodo class_eval, si cambia self per fare riferimento alla classe originale e quando si chiama instance_evalself cambia per fare riferimento alla metaclasse della classe originale.

Definire i metodi mancanti al volo

Un altro pezzo del puzzle della metaprogrammazione è method_missing. Quando si chiama un metodo su un oggetto, Ruby va prima nella classe e cerca i suoi metodi di istanza. Se non trova il metodo lì, continua a cercare nella catena degli antenati. Se Ruby non trova ancora il metodo, chiama un altro metodo chiamato method_missing che è un metodo di istanza di Kernel che ogni oggetto eredita. Dato che siamo sicuri che Ruby chiamerà questo metodo alla fine per i metodi mancanti, possiamo usarlo per implementare alcuni trucchi.

define_method è un metodo definito nella classe Module che potete usare per creare metodi dinamicamente. Per usare define_method, lo si chiama con il nome del nuovo metodo e un blocco dove i parametri del blocco diventano i parametri del nuovo metodo. Qual è la differenza tra usare def per creare un metodo e define_method? Non c’è molta differenza, tranne che potete usare define_method in combinazione con method_missing per scrivere codice DRY. Per essere precisi, potete usare define_method invece di def per manipolare gli scopi quando definite una classe, ma questa è tutta un’altra storia. Diamo un’occhiata ad un semplice esempio:

Questo mostra come define_method è stato usato per creare un metodo di istanza senza usare un def. Tuttavia, c’è molto di più che possiamo fare con loro. Diamo un’occhiata a questo frammento di codice:

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"

Questo codice non è DRY, ma usando define_method possiamo renderlo DRY:

Questo è molto meglio, ma ancora non perfetto. Perché? Se vogliamo aggiungere un nuovo metodo coding_debug per esempio, dobbiamo mettere questo "debug" nell’array. Ma usando method_missing possiamo risolvere questo problema:

Questo pezzo di codice è un po’ complicato, quindi scomponiamolo. Chiamando un metodo che non esiste, si attiva method_missing. Qui, vogliamo creare un nuovo metodo solo quando il nome del metodo inizia con "coding_". Altrimenti chiamiamo semplicemente super per fare il lavoro di segnalazione di un metodo che è effettivamente mancante. E usiamo semplicemente define_method per creare quel nuovo metodo. Questo è tutto! Con questo pezzo di codice possiamo creare letteralmente migliaia di nuovi metodi a partire da "coding_", e questo è ciò che rende il nostro codice DRY. Poiché define_method è privato di Module, abbiamo bisogno di usare send per invocarlo.

Concludendo

Questa è solo la punta dell’iceberg. Per diventare un Ruby Jedi, questo è il punto di partenza. Dopo aver padroneggiato questi mattoni della metaprogrammazione e averne compreso veramente l’essenza, puoi procedere a qualcosa di più complesso, per esempio creare il tuo Domain-specific Language (DSL). Il DSL è un argomento a sé stante, ma questi concetti di base sono un prerequisito per capire gli argomenti avanzati. Alcune delle gemme più usate in Rails sono state costruite in questo modo e probabilmente avete usato il suo DSL senza nemmeno saperlo, come RSpec e ActiveRecord.

Spero che questo articolo possa farvi fare un passo avanti nella comprensione della metaprogrammazione e forse anche nella costruzione del vostro DSL, che potrete usare per codificare in modo più efficiente.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.