Displaying articles with tag

Il Distributive Conjunction Pattern

Posted by Chiaroscuro, Mon May 14 16:43:00 UTC 2007

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:

  1. ricordarci l’array d’origine – altrimenti su cosa andiamo a distribuire?
  2. 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.

1 comment | Filed Under: Linguaggio Metodologia | Tags: