Questo articolo è una sorta di percorso illustrativo all'interno di un'area molto blasonata e forse, in realtà, poco conosciuta e sperimentata di Ruby: la metaprogrammazione. Il termine 'meta-programmazione' si compone del prefisso 'meta' e della parola 'programmazione' e significa «scrivere programmi che creano o manipolano altri programmi (o loro stessi)».
Chi ha già esperienza con Ruby on Rails non troverà difficile far corrispondere alla definizione appena esposta i generatori; infatti invocando:
ruby script/generate model User
lo script Ruby generate
produrrà tutto il codice necessario a definire il modello User
nell'applicazione che stiamo realizzando. I generatori sono un ottimo esempio di metaprogrammazione esterna cioè di programmi che, una volta eseguiti, generano altri programmi.
In questo articolo però andremo ad approfondire la metaprogrammazione interna (detta anche riflessività o reflection), cioè quella caratteristica che permette ad alcuni linguaggi di programmazione di ispezionare e modificare a runtime il proprio codice.
Classi e istanze
Ruby è per sua natura orientato e predisposto all'introspezione. Osservando le API (ad esempio nella classe Object
), è possibile notare tutta una serie di metodi che ci consentono di ispezionare il contenuto dell'oggetto che stiamo creando/manipolando, facciamo qualche esempio:
a = Array.new # [] a.methods # ["send", "delete_if", "index", ... a.class # Array Object.constants # ["Signal", "FalseClass", "FloatDomain...
La vera potenza di questo linguaggio però può essere percepita solamente comprendendo a fondo il modo in cui Ruby struttura e collega classi ed oggetti: partiamo da un modello molto comune:
In questo schema è rappresentata una classe, il quadrato, ed un oggetto istanziato (utilizzeremo sempre linee blu per specificare l'istanziazione), il cerchio. La classe contiene al suo interno le variabili di classe (quelle che cominciano con la doppia chiocciola: @@
) ed i metodi di cui l'oggetto può usufruire. L'oggetto conterrà invece soltanto le sue variabili di istanza (quelle che cominciano con la chiocciola: @
).
Facciamo subito un esempio e supponiamo che la classe dello schema sia User
, così definita:
class User def initialize(name,surname) @name,@surname = name,surname end def full_name "#{name} #{surname}" end end
Istanziamo la classe:
sandro = User.new('Sandro','Paganotti') # istanzazione sandro.instance_variables # ["@surname", "@name"] sandro.full_name # "Sandro Paganotti"
Le due stringhe passate come parametri al costruttore vengono memorizzate all'interno dell'oggetto (nelle variabili @name
e @surname
) mentre il metodo full_name
che manipola queste stringhe (concatenandole) è in realtà memorizzato all'interno della classe User
e viene invocato dall'oggetto.
Metaclassi
Introduciamo un nuovo concetto: le classi in Ruby sono oggetti di altre classi. Quindi la classe User
è in realtà un oggetto istanziato dalla classe Class
come possiamo vedere dal seguente frammento di codice:
sandro = User.new("Sandro","Paganotti") sandro.class # User User.class # Class Class.class # Class
Traduciamo quanto appreso in un nuovo diagramma:
Siamo arrivati al punto critico di questo articolo. La comprensione di quanto segue è alla base di tutte le tecniche di reflection che Ruby mette a disposizione.
Procediamo per gradi, per derivazione da quanto enunciato precedentemente ci dovremmo aspettare che i metodi di cui dispone l'oggetto User
vengano definiti all'interno della classe Class
, così come i metodi per l'oggetto sandro
sono definiti dalla classe User
.
In effetti se esploriamo i metodi descritti all'interno di Class
notiamo, ad esempio, il metodo new
, che viene utilizzato quotidianamente per istanziare oggetti (è proprio il new
che viene eseguito chiamando User.new
).
Mancano però all'appello tutti i metodi di classe che è possibile definire all'interno del codice, come ad esempio:
class User def self.descrizione "Una classe poco utile" end end
Dove vengono memorizzati questi metodi? La risposta è: nella metaclasse.
Cerchiamo di fare un po' di chiarezza. La classe Class
è la classe dalla quale viene istanziata la classe User
, che a sua volta fa da classe per l'istanza sandro
. I metodi definiti nella classe diventano disponibili nell'oggetto (è il caso di full_name
per sandro
e new
per User
), i metodi che invece vengono definiti all'interno della classe stessa, utilizzando il self
(esempio: self.descrizione
) vengono memorizzati nella metaclasse: una classe speciale assegnata automaticamente ad ogni oggetto.
Nella metaclasse, quindi, trovano spazio tutti quei metodi che sono propri della specifica istanza che stiamo usando, descrizione
ad esempio è un metodo dell'istanza User
, ma non delle altre istanze della classe Class
e quindi viene memorizzato nella metaclasse di User
.
Alla luce di quanto detto finora aggiorniamo il diagramma utilizzando i nomi di classe dell'esempio (code>sandro,User
e Class
) e includendo anche il concetto di metaclasse.
Prima di continuare è necessario sapere che Ruby non offre nativamente alcuna funzione per accedere alla metaclasse di un dato oggetto. Fortunatamente whytheluckystiff, uno sviluppatore Ruby conosciuto a livello internazionale, ha sviluppato una gemma chiamata metaid
che, fra le altre cose, aggiunge a tutti gli oggetti un metodo metaclass
che ci permette di accedere e lavorare sulla metclasse di un dato oggetto. È possibile installare tale gemma con il comando:
gem install metaid
oppure inserire nella propria applicazione solo il codice necessario per la funzione metaclass
:
class Object def metaclass class << self self end end end
Ora possiamo inziare a verificare alcune affermazioni fatte in precedenza o esposte nel diagramma.
1. Il metodo di classe descrizione
della classe User
è memorizzato nella metaclasse dell'oggetto User
User.metaclass.instance_methods.grep(/descrizione/)
# => ["descrizione"]
2. Le metaclassi di oggetti che sono gli uni istanze degli altri (Class -> User -> sandro
) sono legate da una relazione di Ereditarietà.
sandro.metaclass.superclass == User.metaclass # true User.metaclass.superclass == Class.metaclass # true Class.metaclass.superclaa == Class.metaclass # true #( La metaclasse di Class eredita da se stessa )
3. Ogni metaclasse è un'istanza della classe Class.
sandro.metaclass.class # Class User.metaclass.class # Class Class.metaclass.class # Class
Esempi pratici
Prima di completare il quadro generale, aggiungendo anche Object
e Module
allo schema, è utile spendere due parole sulle possibilità che la conoscenza di questa struttura ci regala, per farlo esaminiamo alcuni esempi.
Aggiungere un metodo di classe alla classe User
:
User.metaclass.class_eval do
def meteo
"pioggia"
end
end
User.meteo # pioggia
Raggiungere il metodo meteo
partendo dall'istanza sandro
sandro.metaclass.superclass.method(:meteo).call # pioggia
Fare in modo che una classe erediti da User
una funzione customizzata su di uno specifico parametro:
class User
def self.tipologia(tipo)
metaclass.instance_eval do
define_method :chi_sono? do
tipo
end
end
end
end
class Painter < User
tipologia 'Artista'
end
puts Painter.chi_sono? # Artista
Quest'ultimo esempio è più complesso e necessita di una breve spiegazione: nella classe User
definiamo un metodo di classe chiamato tipologia
, tale metodo, quando invocato inietta nella metaclasse dell'oggetto self
un metodo chi_sono?
che ritorna semplicemente la stringa passata come parametro di tipologia
. Due righe più in basso la classe Painter
invoca tipologia con parametro Artista
e ne ricava un metodo chi_sono?
che, se invocato, ritorna Artista
.
Lo schema completo
Siamo finalmente pronti per l'ultimo passo, aggiungiamo al diagramma precedente anche Object
, che come sappiamo è la superclasse di tutte le classi, e Module
.
In questo ultimo diagramma abbiamo inserito la classe/oggetto Object
e la classe Module
, le relazioni scaturite verso gli oggetti inseriti precedentemente sono derivanti da osservazioni sul seguente codice:
User.superclass # Object Object.class # Class Class.superclass # Module Module.superclass # Object Module.class # Class
Conclusioni
Abbiamo illustrato una buona panoramica della struttura di relazioni che si forma intorno ad un qualsiasi applicativo Ruby, conoscere questa struttura è stato per essenziale all'autore per comprendere alcuni 'prodigi' incontrati durante la lettura di programmi Ruby.
A questo modello si affianca una serie di funzioni create appositamente per manipolare ed ispezionare questa fitta rete di relazioni: alcune incontrate e 'sperimentate' in questo articolo, altre da trovare all'interno delle API di Ruby (cercate soprattutto all'interno delle classi Module
, Object
e Kernel
).
Chiaramente di argomenti satelliti a quanto appena trattato ce ne sono tantissimi, ad esempio recuperare e delegare metodi, usare 'macro' e salvare il contesto all'interno del quale è stata eseguita un operazione sono solo alcune delle possibili 'derivazioni' di quanto espresso in questo articolo.