Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Metaprogrammazione e reflection in Ruby

Esploriamo le classi e come istanze di metaclassi
Esploriamo le classi e come istanze di metaclassi
Link copiato negli appunti

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:

Figura 1. Schema classe/istanza
Schema classe/istanza

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:

Figura 2. Classe come istanza di una Metaclasse
Classe come istanza di una Metaclasse

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.

Figura 3. Metaclassi e metodi di istanza
Metaclassi e metodi di istanza

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.

Figura 4. Schema completo con Object e Module
Schema completo con Object 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.


Ti consigliamo anche