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:

  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.

Filed Under: Linguaggio Metodologia | Tags:

Comments

  1. riffraff 02.02.08 / 16PM
    a me l'approccio con metodo intermediario (#all in questo caso, o #should in rspec) piace molto, sarebbe bello si diffondesse :) C'è da notare però che scrivere multiplexer in questo modo contiene sempre un problema implicito, e cioè che il resto del mondo non è cosciente di avere a che fare con una (con?)junction, per cui solo metodi invocati sulla junction possono essere multiplexati, ma non si potrebber avere @assert_equal x, ary.all@ O meglio: sarebbe "una fatica":http://rubyforge.org/projects/junction/ farglielo fare :) Per un'altra propsettiva sull'array-based programming consiglio di guardare F-Script, in cui il _messaging di ordine superiore_ è integrato in un ambiente smalltalk-like con un paio di accortezze sintattiche, purtroppo non replicabili in ruby :/

Have your say

A name is required. You may use HTML in your comments.