Il Distributive Conjunction Pattern
Posted by Chiaroscuro, Mon May 14 16:43:00 UTC 2007
Per molti autori di codice il bello di Ruby stà nella sua capacità di sintesi che permette di comprimere in pochissime istruzioni il significato del proprio codice. Alcune di queste tecniche di sintesi e compattazione del codice vengono definite deep magic, sottolineando sia la chiarezza del risultato, quanto sia oscuro il modo in cui viene ottenuto. Oggi mostrerò come utilizzare un tocco di deep magic per semplificare l’espressione di operazioni che agiscono su più elementi di un array.
Considerate i linguaggi imperativi a cui siamo sempre stati abituati, da visual basic a java, e vediamo come potremmo esprimere, ad esempio, l’azione di capitalizzare tutti gli elementi di un array di stringhe identificato dalla variabile person.
Utilizzando uno stile imperativo in ruby lo potremmo scrivere come:
1 2 3 |
for person in people do person.capitalize! end |
Con ruby possiamo anche appoggiarci ad uno stile più dichiarativo, sfruttando gli iteratori interni degli array:
people.each { |person| person.capitalize! } |
ancora non soddisfatti possiamo utilizzare il distributive conjunction pattern ed ottenere:
people.all.capitalize! |
Questo articolo svela tutti i trucchi su come plasmare questo pattern.
La soluzione in stile dichiarativo che utilizza each non ci soddisfa ancora completamente. Questo codice contiente infatti ancora varie ripetizioni e ridondanze. Consideriamo ad esempio il termine person che deve venir specificato una volta come parametro ed una volta come variabile. Certo, senza l’uso di person non potremmo utilizzare l’iteratore perchè non avremmo alcuna referenza all’oggetto su cui vogliamo andare ad operare.
Tuttavia è possibile immaginare l’aggiunta di un metodo capitalize! alla classe Array. Questo metodo, a sua volta, andrebbe ad invocare capitalize! su ogni suo singolo elemento, utilizzando il proprio iteratore interno.
1 2 3 4 5 |
class Array def capitalize! self.each { |e| e.capitalize! } end end |
Vi vedo già rabbrividire. Non solo perchè oso violare la classe Array con i miei noiosi metodi, totalmente fuori contesto all’interno di un Array, ma anche perchè a questo punto Array dovrebbe venir invasa da un duplicato di qualsiasi metodo io possa voler invocare su un qualsiasi oggetto.
Lasciamo in sospeso il primo punto per un attimo (vi prometto di ritornarci) e affrontiamo il secondo. Possiamo difenderci dall’invasionedei metodi clonati utilizzando un primo tocco di magia dinamica: method_missing. Invece di andare a specificare un doppione per ogni potenziale metodo invocabile su ogni potenziale membro di un array, andiamo a supporre che qualunque metodo invocato sull’array e non riconosciuto dall’array stesso, sia in realtà indirizzato ai suoi elementi.
Proviamo:
1 2 3 4 5 |
class Array def method_missing method, *args self.each { |e| e.send method, *args } end end |
E’ stato un piccolo colpo di tacco, ma già funziona.
puts ["john","mike","sam"].capitalize! |
produce il risultato desiderato:
1 2 3 |
John Mike Sam |
Tramite questa generalizzazione abbiamo anche ottenuto l’effetto secondario di essere in grado di gestire automaticamente i parametri di questi proxy-metodi.
Proviamo a trasformare in ‘1’ tutte le ‘i’ di una lista di nomi (no, non chiedetemi perchè):
puts ["simon","mike","jim"].tr!('i','1') |
ancora successo:
1 2 3 |
s1mon m1ke j1m |
Ora che abbiamo il nostro iteratore automatico possiamo divertirci ad invocare ogni sorta di metodi sui nostri array.
Basteranno però pochi esperimenti per andare a sbattere contro un piccolo problema. Come possiamo, ad esempio, ottenere la lunghezza di tutti gli elementi di un array? Risulta non essere possibile, in quanto il metodo size che dovremmo invocare su ogni elemento, ha già un significato nel contesto di Array dove rappresenta la lunghezza dell’array stesso.
E qui ritorniamo al problema che avevo precedentemente posticipato: i pericoli dell’inquinamento semantico. Da una lato abbiamo trovato un modo rapido ed espressivo per distribuire chiamate agli elementi di un array, mentre dall’altro abbiamo scoperto che questo può portare ad uno scontro di significato tra le esigenze dell’array-ospite e quelle degli oggetti-ospitati.
La soluzione che ho elaborato, e che ho visto essere utilizzata con alcune verianti in diversi progetti, cerca di ottenere un punto di equilibrio tra le due opposte tensioni di iperspecificità e sintesi (a volte ambigua). Ho chiamato questa tecnica Distributive Conjunction Pattern e il suo utilizzo si esplica in:
people.all.capitalize! |
Il codice è espressivo ed è possibile capire al colpo d’occhio l’intento dello sviluppatore. E’ forse meno chiaro risalire a come questo codice possa funzionare.
Intanto vediamo perchè si tratta di una conjunction. Con il termine congiunzione non mi riferisco letteralmente alle congiunzioni dei linguaggi parlati, ma ne reinterpreto in qualche modo lo spirito. Il metodo all è una congiunzione perchè di per se non fa nulla ma genera significato mettendo in relazione il people su cui è chiamato e il capitalize! che è a sua volta invocato sull’oggetto risultante da all.
Perchè questa congiunzione sarebbe distributive? Come la proprietà distributiva trasforma (a + b + c) * X in a*X + b*X + c*X , così una congiunzione distributiva andrà a distribuire un certo metodo su ogni membro dell’array d’origine della congiunzione.
Ora che sappiamo che l’unico scopo di all è di mettere in relazione due termini e distribuire il secondo sul primo, vediamo come implementarlo in pratica.
Il primo passo consiste nell’aggiungere il metodo all ad Array, e delegare a quel qualcosa che verrà ritornato dal metodo all la responsabilità di distribuire tutte le chiamate agli elementi dell’array. Questo qualcosa ritornato da all possiamo allora chiamarlo ArrayDistributor? o ArrayDistributiveConjunction?, per rendere chiaro che si tratta di un oggetto temporaneo utilizzato al solo fine di congiungere l’array con qualcosaltro.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ArrayDistributiveConjunction def initialize ... end end class Array def all ArrayDistributiveConjunction.new end end |
Come sarà strutturata una conjunction? Dobbiamo saperlo anche per capire come poterla inizializzare adeguatamente entro il nuovo metodo all di Array.
Dal momento in cui la conjuction viene generata vive di vita propria e deve essere in grado di gestire tutte le potenziali chiamate indirizzate verso gli elementi dell’array di origine. Dobbiamo quindi:
- ricordarci l’array d’origine – altrimenti su cosa andiamo a distribuire?
- accettare qualunque chiamata – da distribuire per l’appunto sull’array di origine della conjunction.
Affrontiamo il primo punto, adeguando anche la classe Array:
1 2 3 4 5 6 7 8 9 10 11 |
class ArrayDistributiveConjunction def initialize array @array = array end end class Array def all ArrayDistributiveConjunction.new self end end |
non è stato difficile.
Per il secondo punto, invece, possiamo riutilizzare la soluzione del method_missing illustrata in precedenza. L’unica differenza è che in questo caso il contesto della distribuzione non è più l’array stesso, ma un array esterno che viene puntato da un attributo della conjunction:
1 2 3 4 5 6 7 8 9 |
class ArrayDistributiveConjunction def initialize array @array = array end def method_missing method, *args @array.map { |e| e.send method, *args } end end |
In realtà abbiamo applicato un altro piccolo cambiamento, utilizzando l’iteratore map piuttosto che each per poter gestire una più ampia varietà di metodi che non solamente quelli che modificano direttamente gli oggetti su cui operano.
Il codice finale è semplice e breve:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class ArrayDistributiveConjunction def initialize array @array = array end def method_missing method, *args @array.map { |e| e.send method, *args } end end class Array def all ArrayDistributiveConjunction.new self end end |
e fa il lavoro che deve fare con efficienza e senza fronzoli.
Una chiamata diretta alla size di un array
puts ["simon","mike","jim"].size |
genera
3 |
mentre una chiamata a size, distribuita tramite all
puts ["simon","mike","jim"].all.size |
genera un array delle size dei suoi elementi
1 2 3 |
5 4 3 |
Incidentalmente questa soluzione ha un effetto interessante. Oltre a distribuire delle chiamate sugli elementi di un array, potete anche distribuire delle estrazioni dagli stessi. Un pò come una SELECT su una TABLE in SQL, per capirci.
Supponiamo di avere una semplicissima classe Person:
Person = Struct.new :name, :surname, :age |
e una lista people:
1 2 3 4 5 |
people = [
Person.new('john','smith',30),
Person.new('mike','hammer',45),
Person.new('sam','spade',33)
] |
ora, l’uso della distributive conjunction ci permetterà di estrarre al volo una lista dei nomi, cognomi o età delle persone:
1 2 3 |
puts people.all.name # >> ['john','mike','sam'] puts people.all.surname # >> ['smith','hammer','spade'] puts people.all.age # >> [30, 45, 33] |
Abbiamo visto la distributive conjunction, ma vi sono molti altri usi interessanti delle congiunzioni al fine di rendere il proprio codice più leggibile, compatto ed espressivo che esploreremo in futuri articoli.