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.
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_eval
self
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.