Avevamo concluso l'altro articolo
[1]
sottolineando come per Matz,
parafrasando L. Torvalds
"
il principale problema di design è che debba essere
divertente"
.
Iteratori & Blocchi
Un esempio di semplificazione è l'introduzione in ruby degli
Iteratori, un paradigma originario del linguaggio
CLU
[2]
importato in molti altri linguaggi.
Java e C++ hanno classi Iterator, concettualmente simili ma molto
differenti.
Python ha introdotti iteratori CLU-like di recente, e C# dovrebbe introdurli a breve.
Un iteratore è un metodo che accetta in input una funzione, e la
applica ripetutamente passandogli dei parametri.
Ad esempio, per stampare a schermo un array in ruby basta fare:
array.each { |parametro| puts "elemento: "+parametro }
Nota: in realtà basterebbe fare
puts array
, in
quanto puts verifica se l'oggetto è di classe Array e pensa al posto nostro ad
iterare su di esso.
Quello tra parentesi è un
blocco.
Un blocco è una funzione anonima, nota anche come
lambda, ed è
un
oggetto
Proc in ruby.
Si tratta di strutture comuni nei dialetti lisp e di sicuro
familiari anche
a chi programma in Python.
A differenza del Python, Ruby permette di definire lambda anche molto
complesse, e non soltanto one liner.
Inoltre una Proc è una
chiusura, cioè una struttura che
contiene un
espressione ed un collegamento a tutte le variabili esterne al momento
della
definizione dell'espressione, quello che viene definito
lexical
scope.
Perché chiusura? Perché la funzione "si chiude" sopra una
variabile,
trascurando cambiamenti successivi, ad esempio:
iniziale=10
def contatore( n )
proc do
val = n
n = n + 1
val # valore restituito
end
end
c1=contatore(iniziale)
p c1.call #10
p c1.call #11
p c1.call #12
iniziale=30 # non influisce su contatore, perchè questo si è
# "chiuso" su iniziale=10
c2=contatore(iniziale)
p c2.call #30
p c1.call #13 ! non veniamo influenzati
p c2.call #31
Pensate a come avreste realizzato un contatore in altri linguaggi:
dovreste specificare una variabile globale condivisa, oppure creare una
Classe Contatore. Le chiusure semplificano questo processo.
Per ulteriori informazioni si dia un'occhiata a
questa pagina
[3] e collegate.
Le chiusure in ruby sono simili a quelle di common lisp, smalltalk e
scheme; permettono di accedere non solo agli oggetti a cui fanno
riferimento ma anche alle associazioni variabile/oggetto, mentre ad
esempio
in Python si ha accesso soltanto all'oggetto e non
all'associazione.
Dunque, dicevamo che un iteratore usa un blocco di codice e gli
passa
delle variabili. Perché ciò ci è utile?
La risposta è nella pigrizia dei programmatori.
Una volta stabilito che una nostra classe rappresenta una collezione,
ci
basta definire un metodo each e chiunque potrà usarla nello stesso modo
in cui usa qualsiasi altra collezione, sia essa una lista concatenata, un
Hash, un Albero o un Insieme disordinato.
Si lascia insomma ai dati stessi il compito di sapere come devono
essere esplorati, incapsulando al loro interno la logica di visitazione , e
separandola da quella del programma vero e proprio.
In ruby esistono anche molti esempi di funzioni che accettano
blocchi in input, anche se non si tratta di iteratori.
Ad esempio è possibile aprire un file e passargli un blocco di
codice:
File.open ("miofile.txt") { |fd|
fd.readlines.each_with_index { |linea, num|
puts num if linea =~ "ruby"
}
}
Questo stamperà a schermo i numeri di riga nelle quali viene
trovata la
stringa "ruby" nel file "miofile.txt".
Nota: prima che vi stupiate per la presenza di
quell'operatore in
stile Perl, "
=~
" sappiate che in realtà si tratta di un metodo della classe
String, che verifica la corrispondenza con una Regexp.
L'approccio inverso, usando un metodo della classe Regexp, sarebbe
stato di fare "
/ruby/.match (linea)
".
Usando questo tipo di sintassi non dobbiamo preoccuparci di gestire
la chiusura del file.
Il metodo open farà si che l'esecuzione del blocco venga portata a
termine e
poi penserà a "sistemare le cose" per noi.
I metodi che accettano blocchi in ruby sono pervasivi anche perché
è stata
prevista una sintassi nella definizione dei metodi che prevede la loro
attesa ed il loro uso:
def metodo(arg1,...,&blocco)
yield param1,param2,param3,...
end
La presenza di questa sintassi è molto più importante di quello che sembra.
Sebbene altri linguaggi prevedano sistemi simili, non prevedono una specializzazione
della sintassi per supportarli, e di fatto ne rendono meno intuitivo l'utilizzo.
Ancora una volta, ruby cerca di rendere semplice la vita del
programmatore.
OO in Ruby
Tale scelta è visibile anche nelle scelte relative alla OO.
In ruby, come già detto, tutto è una classe. Nonostante ciò possiamo
adottare
un approccio semi-imperativo, se lo desideriamo.
Ad esempio, questo potrebbe sembrare un programma in un linguaggio
imperativo:
def saluto (chi)
return "Salve "+chi+"!"
end
puts saluto ("gente")
In realtà all'esecuzione accadono molte cose dietro le quinte, che
fanno si che tutto funzioni (scrivendo "Salve gente!") pur rimanendo
completamente OO.
Per spiegare come ciò accada abbiamo bisogno di capire come
funzionano le
classi e gli oggetti in ruby.
La prima cosa da dire è che ruby non supporta l'ereditarietà
multipla.
Questa scelta è stata fatta per mantenere il linguaggio semplice, e
perché si
è ritenuto che siano pochi i casi in cui sia necessaria una vera
ereditarietà
multipla (la stessa scelta è stata fatta nel design di Java e del
framework
.NET).
Funzionalità simili sono però ottenibili grazie ai mix-in di moduli, in
maniera simile a ciò che fa Java con l'implementazione delle
Interfacce.
La differenza fondamentale è nella possibilità di definire realmente
delle
funzionalità nei moduli, ed usarli in seguito come semplici package, o
includendone le funzionalità all'interno della propria classe.
Ad esempio: supponiamo di voler creare una classe
LinkedList
.
Faremo si che essa includa il modulo
Enumerable
.
Ci basterà definire il metodo each per ottenere "automagicamente" i
metodi:
- each
- member?
- collect
- detect
- entries
- reject
- each_with_index
- find
- select
- include?
- grep
- map
- find_all
Definendo
<=>
e includendo
Comparable
otterremo "<=", "<", ">", "==", "between?", ">=".
Includendo Enumerable e Comparable otterremo anche "sort", "max" e
"min".
Esistono moduli per includere
funzionalità molto interessanti, pensate ad esempio a un modulo
Persistence
Layer che definisca per noi save/delete/retrieve.
L'esistenza di moduli di questo tipo spiega anche la semplicità di
poter
scrivere "
puts stringa
" invece di qualcosa come
"
System.out.println(stringa)
".
Semplicemente, alcune funzionalità vengono definite nel modulo
Kernel
ed incluse direttamente in
Object
, la
madre
di tutte le classi, ed ereditate poi da quelle definite da noi.
Per poter spiegare il nostro esempio pseudo imperativo manca ancora
un
fattore però.
La possibilità di definire dei
metodi singleton per ogni
oggetto.
Un metodo di questo tipo è un metodo aggiunto ad un oggetto specifico
dopo la
sua creazione.
Ad esempio, se dovessimo gestire un sistema con oggetti Dipendente e
CapoReparto, dove l'unica differenza tra le due classi è un
metodo
licenzia_operaio(nome)
non avremmo la necessità di
definire una nuova classe, ci basterebbe definire un metodo per
l'oggetto
Dipendente che ci interessa (ogni CapoReparto è ovviamente un
dipendente).
Il poter definire metodi di questo tipo acquista una nuova luce se
si
considera che ogni classe è un'istanza della classe Class.
Possiamo dunque modificare a runtime intere classi (e moduli)
specializzandole per i nostri scopi senza la necessità di subclassing,
e
facendo uso quindi di tutte le caratteristiche integrate nel
linguaggio.
Ad
esempio possiamo applicare a String un metodo "to_hex", continuando ad
usare
la normale definizione integrata delle stringhe (cioè l'apertura e la
chiusura delle virgolette).
Inoltre possiamo modificare un'intera gerarchia di oggetti, ad
esempio
modificare Numeric, la superclasse di Integer, Fixnum, Float, Bignum,
...
Un esempio semplice ed imperfetto per aggiungere uno pseudo metodo
di
rappresentazione in xml a qualunque cosa (non funziona con le
collezioni, e
probabilmente con molte altre cose):
class Object
def to_xml
print "< ",self.class," id=", self.id ,">\n"
puts self
instance_variables.each do |k|
eval(k).to_xml
end
print "</",self.class,">\n"
end
end
e uno spezzone di prova:
class Mia
def initialize(x,y)
@x=x
@y=y
end
end
class Tua <Mia
end
a=Mia.new "ciaO",5 ; b=Tua.new a , 30.0
b.to_xml
che darà in output una cosa del genere:
< Tua id=21832680>
#<Tua:0x29a47d0>
< Float id=21832920>
30.0
</Float>
< Mia id=21832692>
#<Mia:0x29a47e8>
< Fixnum id=11>
5
</Fixnum>
< String id=21832788>
ciaO
</String>
</Mia>
</Tua>
I singleton methods e l'idea di classi ed oggetti aperti non sono
una cosa
particolarmente nuova nell'informatica. Molti altri linguaggi come
lisp/CLOS
, Dylan, etc. supportano questo paradigma, in quanto permette di unire
una
grande potenza espressiva con una buona semplicità implementativa.
Avendo compreso come funzionano i metodi singleton e i mix-in, si
può
spiegare anche come sia possibile definire in ruby un metodo al di
fuori di
una classe: ciò che stiamo facendo è definire un metodo singleton
privato
all'interno del modulo Kernel.
In questa maniera possiamo adoperarlo all'interno di qualsiasi classe
esistente, ma non possiamo invocarlo con la sintassi
obj.metodo
.
Ma tornando al nostro esempio: dove stiamo definendo quella roba?
Non dovrebbe essere possibile essere al di fuori di nessuna classe!
Ebbene, anche in questo caso ruby lavora dietro le quinte, definendo
per noi
un oggetto chiamato "
main
", all'interno del quale accade
tutto
il lavoro vero.
Ancora una volta, ruby gestisce tutto al posto nostro, senza incrinare
minimamente la propria purezza.
Ruby e la dinamicità
L'ultima caratteristica di ruby che è giusto sottolineare è la sua
dinamicità.
Ruby trae molta ispirazione dai vari dialetti del Lisp e da Smalltalk,
ed
offre una serie di strumento per permettere un'estrema dinamicità del
codice.
In primo luogo esistono varie forme per la funzione
eval()
,
offrendo anche
instance_eval()
e
module_eval()
cioè
versioni della chiamata che hanno come scope soltanto l'oggetto o il
modulo.
Ad esempio grazie ad instance_eval è possibile fare cose del
genere
def with(object, &block)
object.instance_eval(&block)
end
with "1" do
puts "La mia lunghezza e` #{length}"
end
Inoltre ruby ci fornisce numerosi modi per sapere cosa sta
succedendo.
Ad esempio, esistono una serie di metodi che possono venire richiamati
in
occasione di alcuni eventi, come
Module#method_added
che notifica l'aggiunta di metodi
d'istanza
,
extend_object
che notifica l'avvenuto mix-in di un modulo,
Class#inherited
che avverte se la nostra classe viene
subclassata, etc..
Probabilmente il più interessante è
method_missing
.
Questo
è un metodo che , se definito, verrà richiamato tutte le volte che
viene
richiesto ai nostri oggetti un metodo che non possiedono.
Ciò permette di fare cose interessanti, ad esempio possiamo definire
una
classe in questo modo:
class LoggerObj
def initialize classe, *args
@obj = classe.new *args
end
def method_missing (method, *args, &block)
@obj.send (method, *args, &block)
log_method (method)
end
def log_method(met)
print "chiamato metodo : " , met
end
end
Ovviamente potremmo fare cose più utili che stampare a schermo,
potremmo salvare in un log ogni modifica all'oggetto, oppure potremmo decidere
di serializzare ogni comando (il che potrebbe risultare utile nella
realizzazione di un prevalence layer).
Possiamo anche vedere method_missing come un metodo ottimale per
l'implementazione del Delegation pattern.
Avendo come background linguaggi come Java, piuttosto statici, si
potrebbe
pensare che oggetti & dinamicità siano una coppia impossibile. Ruby
ci dimostra, (sempre nella tradizione di Smalltalk e CLOS) che ciò non è
sempre vero:
>> [Array,Hash,Object].each do |e|
?> def e.met
>> puts "si definiscono metodi cosi' ?".chop << 33
>> end
>> end
=> [Array, Hash, Object]
>> Object.met; Array.met; Hash.met
si definiscono metodi cosi' !
si definiscono metodi cosi' !
si definiscono metodi cosi' !
In questo esempio abbiamo ottenuto
una definizione a runtime di
metodi
singleton su oggetti di classe Class.
In cinque righe di codice.
L'ultima delle caratteristiche "lispish" di ruby è l'esistenza delle
continuazioni.
Una continuazione è una sorta di bivio.
Immaginate di essere ad un incrocio, non sapendo quale strada sia
migliore
create una continuazione:
siete sulla strada, e trovate un bivio
0 generate una continuazione "mio_bivio"
| \ 1 ---sinistra
| |---un cane vi morde
| \---dovevate andare a destra
| \--- create la continuzione "morso"
| \---tornate a mio_bivio e andate a destra
| \ 3 --- dopo essere stati morsi andate avanti ancora
| | per questa strada
| \--- ma subito venite investiti da un auto...
|
| 2 ---sinistra == male
\---andate a destra
\---incontrate una pantera
\---meglio la sinistra!
\--- generate "pantera" e tornate a "morso"
e così via.
Se ancora non vi è chiaro, pensate alle Continuazioni come alla
superclasse della classe GestioneEccezioni.
La parte interessante è che tutto ciò che accade all'interno di una
continuazione viene cancellato, come se non fosse mai accaduto, nel
caso si torni ad una precedente.
Le continuazioni in ruby sono comunque piuttosto inefficienti (anche
se
forse in Ruby 2.0 non lo saranno più ..).
D'altronde conviene impararle perché saranno presenti anche in Perl6
(e pare che siano in qualche maniera presenti anche in C#)
RealWorldRuby
Ma dove è possibile usare veramente Ruby ?
Si potrebbe avere l'impressione, vedendo Ruby per la prima volta, che
si
tratti di una specie di linguaggio giocattolo
e che il suo ambito applicativo debba limitarsi a compiti dello stesso
genere.
Ruby è ideale per i compiti nei quali vengono applicati gli altri
linguaggi di scripting: amministrazione di sistema,
automazione di compiti ripetitivi, programmini scritti sul momento per
risolvere semplici problemi (ad esempio sostituire le occorrenze di una
parola con un altra in un testo) e cose simili.
Ruby brilla anche nell'organizzazione di strutture web, non solo
semplici
CGI ma anche strutture complesse, soprattutto grazie alla sua purezza
OO.
Apache + mod_ruby o FastCGI permettono buone performance, ed esistono
librerie
e framework per lo sviluppo web già abbastanza avanzate.
Ruby offre poi un discreto supporto per applicazioni di tipo
scientifico, grazie alla libreria standard matematica, e ai pacchetti
Algebra ed NArray
[4].
Sebbene i principi della OO siano abbastanza differenti da quelli
della
matematica Ruby permette di usare numeri a precisione multipla,
Complessi,
Matrici, etc. in maniera del tutto naturale. Il modulo standard
MathN
permette di far coesistere questi tipi, e di operare su di essi in
maniera
"matematica" e non "a forza di cast" :
>> require 'mathn'
=> true
>> due_i=Complex.new 0,2
=> Complex(0, 2)
>> due_terzi= Rational.new 2 , 3
=> 2/3
>> num= due_terzi+due_i
=> Complex(2/3, 2)
>> num+Math::PI
=> Complex(3.80825932, 2)
>> num= num.real
=> 3.80825932
>> num / 0
=> Infinity
Ruby offre poi un'ottima soluzione per l'estensione di applicazioni
esistenti scritte in altri linguaggi.
Ad esempio DataVision, un tool simile a Crystal Report è stato portato
da
Ruby a Java mantenendo Ruby come linguaggio di formulazione, grazie a
un interprete Ruby basato sulla JVM, chiamato JRuby.
Il nuovo sistema di configurazione del Kernel di Linux, KConfig,
presenta
già da tempo un'interfaccia verso Ruby
[5],
ed è molto
semplice usare ruby come interprete "embedded" e quindi come linguaggio
di scripting per applicazioni pi grandi
[6]
Inoltre ruby può essere un'ottima soluzione per la prototipizzazione
di applicazioni.
È piuttosto naturale scrivere applicazioni in ruby tralasciando i
fattori legati alla performance, fornendo un Working Prototype che possa essere
in seguito implementato in linguaggi più efficienti, ad esempio C++ o
ObjC.
Tra l'altro in questa direzione esistono progetti per permettere una
semi-automazione del processo di generazione del codice.
Ruby si pone dunque come uno strumento ottimale nell'ottica di uno
sviluppo di tipo agile, e non a caso Dave Thomas ed Andy Hunt (autori
di
Programming Ruby
[10] che è un po' la bibbia del linguaggio, ed è
disponibile
online) sono tra gli autori dell'Agile Manifesto
[7].
Va in questo senso anche la scelta di includere le librerie Test::*
come
standard nella distribuzione 1.8, e la presenza di una intera suite di
test
per l'interprete già dal 1995.
Inoltre potreste scoprire che esistono applicazioni scritte in ruby
sotto
i vostri occhi di cui non avete mai saputo nulla, ad esempio
portupgrade, un
sistema per freebsd molto usato che permette di automatizzare
l'aggiornamento
dei pacchetti installati.
Ovviamente non è tutto oro quello che luccica.
Alcune librerie importanti sono ancora immature, e non si può comunque
supporre (purtroppo) di usare ruby per qualunque scopo. Ci sarà sempre
bisogno di scrivere librerie per processare grandi quantità di dati in
C, o
magari di avere a che fare con grandi sistemi legacy che richiedono
l'uso di
RPG e COBOL, ma si può sperare di vedere una diffusione del linguaggio
verso
ambiti sempre nuovi.
Ed in fondo, quando programmare è un piacere si può farlo anche se
non c'è
n'è bisogno.