Beruflich Dokumente
Kultur Dokumente
1. 1. Introduzione
La storia di Ruby, la sua diffusione e i motivi del suo successo
2. 2. Caratteristiche di Ruby
Le caratteristiche principali del linguaggio e i principi alla base della sua
programmazione
Gli strumenti
1. 3. L'interprete ruby e Interactive Ruby
I principale attrezzi di lavoro di Ruby: l'interprete ruby e la shell di interazione irb
2. 4. Rdoc, Ri, eRuby e Testrb
La documentazione, la gestione di pagine dinamiche in HTML con Ruby e lo
strumento di test
3. 5. RubyGems e gli IDE per Ruby
Il framework Rubygems e una panoramica dei principali ambienti di sviluppo
disponibili per Ruby
4. 6. Installazione e configurazione
Come installare Ruby su Linux, Windows e Mac
Convenzioni e programmazione OO
1. 7. Convenzioni in Ruby
Le basi del linguaggio Ruby: i nomi, i commenti al codice e la documentazione
2. 8. La programmazione ad oggetti
Introduzione alla programmazione ad oggetti: le classi, i metodi e gli attributi
3. 9. Ruby e gli oggetti - I
La programmazione ad oggetti dal punto di vista di Ruby: esempi commentati di
codice
4. 10. Ruby e gli oggetti - II
La programmazione ad oggetti dal punto di vista di Ruby: uso di attr e i suoi
complementi
I tipi di dati
1. 11. Tipi di dati: numeri
I tipi fondamentali di un linguaggio di programmazione: la gestione dei numeri
2. 12. Tipi di dati: stringhe
I tipi fondamentali di un linguaggio di programmazione: le stringhe
3. 13. Blocchi e iteratori
Come eseguire cicli di codice in Ruby attraverso l'uso di blocchi e di iteratori
4. 14. Array - I
L'uso degli Array all'interno di Ruby: i metodi più comuni
5. 15. Array - II
Array in Ruby: altri metodi e i principali iteratori applicabili alle sue istanze
6. 16. Hash
Come Ruby gestisce gli Hash, gli indici per diversi tipi di dati
Strutture di controllo ed ereditarietà
1. 17. If e unless
I primi costrutti per eseguire le istruzioni condizionali in Ruby: if e unless
2. 18. Case
Eseguire le istruzioni condizionali in modo più chiaro ed elegante con case
3. 19. While, until e for
La gestione dei cicli in Ruby con While e Until e con for
4. 20. Uscire dai cicli
Come terminare l'esecuzione dei cicli con l'istruzioni break o saltare le iterazioni con
next
5. 21. Ereditarietà singola
Approfondimento sulle classi e sulla programmazione ad oggetti: l'ereditarietà singola
6. 22. Ereditarietà multipla
Approfondimento sulle classi e sulla programmazione ad oggetti: l'ereditarietà
multipla
L'interprete Ruby
Innanzitutto c'è l'interprete Ruby che ovviamente si occupa dell'esecuzione dei programmi. Queste
sono le principali opzioni, visualizzabili attraverso l'opzione -help.
$ ruby --help
Vediamone in dettaglio alcune tra quelle che ci possono tornare utili in fase di apprendimento del
linguaggio:
• -c: viene controllata la sintassi dello script, che però non viene eseguito. In caso di successo
viene fuori un rassicurante "Syntax OK".
• -d, --debug: da utilizzare in fase di debug, la variabile $DEBUG è impostata a "true".
• -v, --verbose: mostra informazioni addizionali in output (versione dell'interprete,
warning in fase di compilazione, etc.).
• -w: come -v ma non viene stampata la versione di Ruby, inoltre se non viene indicato il
nome del programma da linea di comando lo legge dallo standard input.
• -e comando: viene eseguito il comando passato come argomento all'opzione.
Per una trattazione completa di tutte le voci si faccia riferimento alla pagina del manuale. Oltre che
attraverso le opzioni da riga di comando è possibile modificare il comportamento dell'interprete
operando sulle variabili d'ambiente. Queste sono le principali variabili utilizzate da Ruby:
• RUBYLIB: lista di directory contenenti librerie aggiunte a quelle standard.
• RUBYOPT: opzioni addizionali da passare all'interprete.
• RUBYPATH: lista di directory nel quale Ruby cerca le applicazioni quando è specificato il
flag -S.
• RUBYSHELL: persorso della shell di sistema, valido solo per piattaforme Windows e OS/2.
Interactive Ruby
L'Interactive Ruby, in breve irb, è una shell che permette di sperimentare in tempo reale col
linguaggio Ruby. Risulta particolarmente utile sia in fase di apprendimento sia in fase di
"sperimentazione". Oltre ad essere un interprete interattivo fornisce alcune utili funzionalità tra le
quali spicca la "Tab Completition", la possibilità di creare sottosessioni e la presenza di alcuni
comandi di controllo (jobs, fg, cb, conf, kill, ...).
Come l'interprete anche irb prevede opzioni da linea di comando:
$ irb --help
Spendiamo qualche parola in più su irb, che utilizzeremo molto nel corso della guida. Vediamo
come va eseguito.
Gli utenti di Linux, e dei sistemi UNIX-like, devono semplicemente aprire un terminale qualsiasi e
scrivere "irb" seguito da una o più opzioni. In ambiente Windows è possibile eseguire fxri
(Interactive Ruby Help & Console) che oltre alla shell irb ci mostra un utile browser della
documentazione (come mostrato in figura 1). In alternativa è possibile accedere a irb anche
attraverso FreeRIDE, attivando la vista attraverso l'apposita icona "IRB" oppure selezionandola nel
menu "View".
eRuby
eRuby (embedded Ruby) è uno strumento che permette di inserire codice ruby all'interno di altri
file, ad esempio nei file HTML creando di fatto delle pagine dinamiche. Ci sono due
implementazioni di eRuby: erb ed eruby. Diamo uno sguardo ad erb che viene distribuito con Ruby.
In pratica erb analizza il file che gli viene passato come argomento alla ricerca di particolari
delimitatori. In particolare quando incontra qualcosa del genere
<% istruzioni ruby %>
valuta l'espressione e ne restituisce il valore; infine ogni linea che inizia col carattere "%" viene
valutata come codice ruby e <# ... %> rappresenta un commento.
Ad esempio se passiamo a erb il testo seguente
% str = "aoxomoxoa"
<%= str %> al contrario si legge <%= str.reverse %>
Testrb
Un semplice esecutore di test (Test::Unit); dispone di diverse interfacce grafiche molto
minimali (tk, gtk, fox) e di una modalità da linea di comando.
Le librerie di base
Oltre ai programmi appena visti, installando ruby in uno dei modi che vedremo nel prossimo
capitolo, otterremo anche le librerie di base. In ambiente Linux si troveranno in
/usr/lib/ruby/ mentre sotto Windows, se si sceglie il percorso consigliato dall'installer,
saranno nella directory c:\ruby\lib\ruby\. In tali directory di norma si trovano 3
sottodirectory:
• 1.8: contiene i moduli e le estensioni standard
• gems: contiene le librerie e le applicazioni installate con l'utility RubyGems
• site_ruby: contiene i moduli e le estensioni di terze parti, aggiunte dallo sviluppatore e
che non fanno parte della distribuzione base.
IDE
Esistono diversi ambienti di sviluppo (IDE, Integrated Development Environments), sia pensati
espressamente per Ruby sia adattati da altri linguaggi. Tra i principali troviamo:
• FreeRIDE: progetto open source, scritto interamente in Ruby. L'unico difetto è una certa
lentezza nei rilasci di nuove versioni e nell'aggiunta di nuove caratteristiche.
• Ruby Development Tool (RDT): plug-in per Eclipse, quindi porta con se tutti i difetti e i
pregi di eclipse.
• Komodo: progetto commerciale di ActiveState, è uno degli IDE più completi e, ad oggi,
l'unico a comprendere un GUI builder integrato. Komodo Editor è invece liberamente
scaricabile.
• ArachnoRuby: progetto commerciale, non è ancora completamente stabile e richiede
requisiti minimi di sistema abbastanza alti. Inoltre lo sviluppo della versione Linux procede
più lentamente di quella per Windows, mentre la versione per Mac OS X non è stata ancora
rilasciata.
• Ruby Development Environment (RDE): progetto non commerciale ancora in fase di
sviluppo. Esiste solo per Windows.
Oltre agli IDE appena visti sono molti utilizzati dalla comunità Ruby anche alcuni editor con
caratteristiche avanzate per gli sviluppatori. A tale proposito vanno sicuramente citati, oltre ai
classici Vim ed Emacs, SciTE e TextMate il primo open source e disponibile per Windows e Linux
mentre il secondo commerciale e disponibile solo per Mac OS X.
Altre implementazioni
Concludiamo questo capitolo citando le altre implementazioni del linguaggio Ruby. Su tutte va
ricordato per completezza e importanza JRuby, un'implementazione del Ruby scritta in Java. Con
JRuby è possibile, tra l'altro, chiamare gli oggetti Java dall'interno delle applicazioni Ruby e quindi
sfruttare tutti i vantaggi del framework di casa Sun. Esistono anche implementazioni, però non
ancora mature, per il framework .NET e sono RubyCLR e Ruby.NET.
Dopo questa breve carrellata dei principali strumenti, che useremo nel corso della guida, andiamo a
vedere come installarli sul nostro sistema qualora non siano già presenti.
Installazione e configurazione
Anche se Ruby è stato sviluppato in, e per un, ambiente Linux è possibile utilizzarlo con la maggior
parte dei sistemi operativi esistenti, come ad esempio UNIX (compresi i *BSD), Microsoft
Windows e DOS, Mac OS X, BeOS, Amiga, Acorn Risc OS, OS/2 e Syllable. La seguente guida fa
riferimento a Linux, ma non ci dovrebbero essere particolari differenze con lo sviluppo in altri
ambienti. Comunque in presenza di notevoli differenze saranno illustrate le varie alternative.
Linux
Gli utenti Linux, e degli altri sistemi UNIX-like, come al solito hanno due alternative per installare
un programma, possono compilarlo dai sorgenti oppure utilizzare il pacchetto precompilato della
propria distribuzione. Sorvolando per ovvie ragioni sull'installazione dei pacchetti andiamo a vedere
come si installa Ruby a partire dai sorgenti. Innanzitutto occorre recuperare il tarball con i sorgenti
dal sito principale.
Ne esistono tre versioni, c'è la versione Stabile, la versione "Stable Snapshot" che contiene l'ultima
versione stabile del CVS e infine c'è la "Nightly Snaphot" che è l'ultima versione del CVS e
potrebbe risultare altamente instabile. Una volta scaricati i sorgenti ci si trova davanti alla classica
installazione UNIX, ovvero configure && make && make install, ad ogni modo
maggiori informazioni si trovano nel file README.
Windows
Anche gli utenti Windows hanno diverse possibilità, possono installare i binari oppure utilizzare il
One Click Installer.
Installando il "Ruby Installer for Windows", che è la scelta consigliata, avremo in poche mosse
un'ambiente di programmazione completo. Precisamente troveremo sul nostro disco, oltre
ovviamente all'interprete Ruby completo di sorgenti con le sue utility e le librerie di base, anche le
utility RubyGems e Rake, alcune utili librerie come FxRuby, OpenGL, XMLParser, HTMLParser,
RubyDBI, la prima edizione del libro "Programming Ruby", e utili tool come l'IDE FreeRIDE (in
figura 2) e l'editor SciTE.
Figura 2: FreeRIDE, l'IDE gratuito per Ruby
Mac
Dalla versione 10.5 di MacOS X Ruby è installato di default. Per le versioni precedenti è possibile
usare i DarwinPorts ed installarlo con un semplice "port install ruby".
RubyGems
I sorgenti di RubyGems sono disponibili su Internet e si installano semplicemente con il comando
"ruby setup.rb".
Altro
Infine è possibile provare Ruby senza installare nulla, basta puntare il proprio browser web su Try
Ruby!. A questo indirizzo si trova una shell interattiva di Ruby pronta all'uso con tanto di tutorial di
base per i primi passi.
Dopo questa necessaria introduzione a Ruby e ai suoi gioielli, cominceremo dalla prossima puntata
a illustrare il linguaggio vero e proprio.
Convenzioni in Ruby
Prima di introdurre i principali aspetti del linguaggio diamo uno sguardo veloce alle principali
convenzioni sui nomi e sui commenti. Altre convenzioni, e semplici suggerimenti sullo stile,
verranno introdotte durante lo svolgimento della guida.
Nomi
Iniziamo a vedere le convenzioni sui nomi delle variabili, dei metodi e delle classi. Di seguito una
rapida carrellata con degli esempi esplicativi:
• Le variabili locali devono iniziare con una lettera minuscola o col carattere underscore.
• Le variabili globali con il segno $.
• Le variabili di istanza con il segno @.
• Le variabili di classe con due segni @.
• Le costanti con una lettera maiuscola.
Ecco degli esempi:
nome = "Matz" # nome è una variabile locale
Mesi = 12 # Mesi è una costante
MESI = 12 # anche MESI è una costante
@linguaggio = "ruby" # @linguaggio è una variabile di istanza
@Linguaggio = "ruby" # anche @Linguaggio è una variabile di istanza
$anno = 2007 # $anno è una variabile globale
@@counter = 0 # @@counter è una variabile di classe
Inoltre i nomi delle classi, e dei moduli, devono iniziare con la lettera maiuscola, mentre i nomi dei
metodi e dei suoi parametri devono iniziare per lettera minuscola o per underscore "_". Ad esempio:
module Geometria
class Quadrato
def disegna
Le parole chiave del Ruby, che come accade per tutti i linguaggi non possono essere utilizzate in
nessun altro modo, sono:
BEGIN END alias and begin break case class
def defined? do else elsif end ensure
false for if in module next nil not
or redo rescue retry return self super then
true undef unless until when while yield
Commenti
I commenti sono identificati dal segno #, è considerato commento tutto quello che lo segue fino alla
fine della riga:
a = 1 # Questo è un commento.
# L'intera riga è un commento
In alternativa è possibile inserire più righe di commento tra i tag =begin e =end, in questo modo:
=begin
Questo è un commento
su più righe
=end
È possibile anche inserire dei commenti che saranno utilizzati per generare documentazione RDoc;
torneremo su questo argomento in un prossimo capitolo.
Documentazione
Come accennato prima, uno strumento fondamentale in fase di apprendimento è ri che prende come
argomento il nome di una classe, di un modulo o di un metodo e ne mostra a video una breve
descrizione con tanto di esempi. Ad esempio per sapere cosa fa e come si usa il metodo reverse
della classe String, che abbiamo usato prima, basta scrivere:
# ri String.reverse
--------------------------------------------------------- String#reverse
str.reverse => new_str
------------------------------------------------------------------------
Returns a new string with the characters from _str_ in reverse
order.
In alternativa al punto si può utilizzare anche il carattere "#" (ri String#reverse); per i
metodi di classe si usa invece la forma con i due punti "::". Se non si conosce il nome esatto è
possibile anche fornire come argomento una indicazione parziale, ad esempio
# ri String.re
More than one method matched your request. You can refine
your search by asking for information on one of:
In questo caso ci viene fornito in output una lista di possibili scelte. Torniamo un attimo ai nomi dei
metodi; il lettore attento avrà di certo notato nell'output precedente la presenza, oltre a reverse,
del metodo reverse! e si starà chiedendo qual'è la differenza tra i due. In Ruby c'è una
convenzione che prevede che i metodi che terminano con il punto esclamativo siano metodi
distruttivi che non creano una nuova istanza ma modificano direttamente il valore. Analogamente ci
sono i metodi di verifica, i cui nomi terminano per punto interrogativo, che svolgono in genere
funzioni di verifica e restituiscono un valore booleano.
Infine una breve nota ripresa da Programming Ruby di Dave Thomas. Aggiungendo questa
funzione
def ri(*names)
system(%{ri #{names.map {|name| name.to_s}.join(" ")}})
end
La programmazione ad oggetti
Poiché in apertura abbiamo detto che in Ruby "ogni cosa è un oggetto", e dato che non è uno
slogan, prima di muoverci in qualsiasi direzione andiamo a vedere cosa sono gli oggetti.
Per permettere a tutti i lettori, anche a quelli che non sono pratici di programmazione orientata agli
oggetti, di apprezzare al meglio il linguaggio, prima di affrontare l'argomento dal punto di vista del
Ruby lo tratteremo in modo generico e molto elementare. Ovviamente chi ha già dimestichezza con
gli oggetti può saltare a piè pari questo paragrafo. Su HTML.it è presente una guida approfondita
alla programmazione ad oggetti.
Nella programmazione OO, come nel mondo reale, un oggetto ha le proprie caratteristiche e i propri
comportamenti. Ad esempio il mio router ha una marca, un modello e un numero di serie e fa delle
determinate cose (ad esempio può connettersi e disconnettersi).
È possibile "comunicare" con gli oggetti attraverso i messaggi, che non sono altro che richieste di
esecuzione di un'operazione. Un'operazione è qualcosa che l'oggetto destinatario è in grado di
eseguire, ad esempio possiamo chiedere al nostro router di connettersi e di disconnettersi.
Informaticamente parlando, per poter creare un oggetto occorre definire una classe che ne definisce
i metodi e gli attributi; in parole povere una classe è un insieme di oggetti simili che si
comportano allo stesso modo e sono caratterizzati dagli stessi attributi.
Diminuendo il grado di astrazione e tornando alla programmazione vera e propria possiamo dire che
un oggetto può essere visto come un contenitore di dati (che a questo punto possiamo identificare
con delle semplici variabili) dotato di una serie di funzioni (i metodi) che definiscono un'interfaccia
all'oggetto stesso.
class Camion
def initialize
puts "Sono un nuovo camion"
end
end
una volta definite le classi possiamo istanziarne degli oggetti. In irb, dopo aver creato le classi
scriviamo:
> ca = CarroArmato.new
Sono un nuovo carro armato
=> #<CarroArmato:0xb7d327fc>
> c = Camion.new
Sono un nuovo camion
=> #<Camion:0xb7d07e78>
Vediamo in ordine cosa abbiamo fatto. Innanzitutto abbiamo creato la classe usando la parola
chiave class seguita dal nome e abbiamo terminato il codice relativo alla nostra classe con la
parola chiave end. Abbiamo quindi creato il metodo initialize che viene chiamato quando
istanziamo un nuovo oggetto con il metodo new. initialize è quello che in OOP si dice un
costruttore che viene invocato ogni volta che si crea un nuovo oggetto. Alcuni linguaggi orientati
agli oggetti come il C++ contemplano anche il metodo distruttore della classe che, invocato al
momento del rilascio dell'istanza di un oggetto, si occupa tra le altre cose di de-allocare la memoria
impegnata dall'oggetto. Nei linguaggi moderni, come Ruby, Java o C#, le attività generalmente
svolte dal distruttore vengono svolte da sistemi di garbage collection, nel caso di Ruby è l'interprete
a farsene carico.
Tornando al nostro esempio, initialize è un normale metodo ed è definito dalle parole chiave
def, seguita dal nome, e dal solito end. Essendo un esempio banale il costruttore non fa altro che
dare notizia dell'avvenuta creazione dell'oggetto stampando a video una stringa utilizzando la
funzione puts (put string).
Per fare un esempio meno banale e per vedere come si passano i parametri ad un metodo
miglioriamo le nostre classi. Supponiamo di voler definire al momento della creazione degli oggetti
alcuni parametri caratteristici. Ad esempio se vogliamo indicare quanto carburante e quanti colpi
avrà il nostro carro armato al momento della creazione ci basta scrivere:
class CarroArmato
def initialize (colpi, carburante)
@carburante = carburante
@colpi = colpi
end
end
Analogamente per i camion, con la differenza che invece dei colpi ci saranno dei posti per i
passeggeri:
class Camion
def initialize (posti, carburante)
@carburante = carburante
@posti = posti
end
end
Come abbiamo visto nel capitolo precedente, @carburante, @colpi e @posti sono delle
variabili d'istanza e sono visibili da tutti i metodi della classe.
In questo modo alla creazione dei nostri nuovi oggetti vanno passati i parametri relativi al
carburante, ai colpi e ai posti:
> ca = CarroArmato.new(10, 100)
=> #<CarroArmato:0xb7d035fc @colpi=10, @carburante=100>
c = Camion.new(20, 100)
=> #<Camion:0xb7ce046c @posti=20, @carburante=100>
@carburante, @colpi e @posti sono detti anche attributi e definiscono come la classe è
vista dall'esterno. Per poter accedere, in lettura, a tali campi occorre creare dei metodi accessori del
tipo:
class CarroArmato
def carburante
@carburante
end
def colpi
@colpi
end
end
Per poter accedere agli attributi anche in scrittura occorre definire anche in questo caso un apposito
metodo:
class CarroArmato
def carburante=(carburante)
@carburante = carburante
end
def colpi=(colpi)
@colpi = colpi
end
end
class Camion
attr :posti, true
attr :carburante, true
Il valore true dopo il nome dell'attributo sta ad indicare che la variabile è anche scrivibile, in
alternativa avremmo potuto usare attr_reader e attr_writer oppure attr_accessor
che è sinonimo di "attr nome_simbolo, true", ovvero scrivere
attr_accessor :posti, :carburante
Questo breve esempio mostra tutta l'eleganza del Ruby che fa di tutto per venire in contro alle
necessità dello sviluppatore senza perdere omogeneità e leggibilità.
In realtà nel nostro caso è meglio permettere solo la lettura degli attributi e creare ad esempio un
metodo per la gestione del carburante. La nostra classe Camion diventa dunque:
class Camion
attr_reader :posti, :carburante
possiamo istanziare nuovi oggetti anche passando un solo parametro, per il secondo argomento
viene considerato il valore di default
> ca = CarroArmato.new(50)
=> #
Quando si chiama un metodo, l'uso delle parentesi è facoltativo, ovvero è corretto anche scrivere:
> ca = Camion.new 50
=> #
Per ora, per evitare confusione fermiamoci qui, torneremo di seguito sugli oggetti trattando
l'ereditarietà, il polimorfismo e altri aspetti avanzati.
Per rappresentare numeri non decimali va fatto precedere al numero vero e proprio un indicatore di
base (0b per i binari, 0 per gli ottali, 0d per i decimali e 0x per gli esadecimali), ad esempio:
> 10 # i numeri decimali sono preceduti da 0d che però può essere omesso
=> 10
> 010 # i numeri ottali sono preceduti da uno 0
=> 8
> 0x10 # i numeri esadecimali sono preceduti da 0x
=> 16
> 0b10 # i numeri binari sono preceduti da 0b
=> 2
Essendo i numeri oggetti di una determinata classe possiamo applicare ad essi i metodi previsti
dalle rispettive classi. I principali metodi previsti da Fixnum e Bignum sono:
• operazioni aritmetiche di base (+, -, *, /, div, %, modulo, **, - unario)
• operazioni sui bit (~, |, &, ^, <<, >>)
• altre operazioni aritmetiche:
• valore assoluto (abs): -123.abs -> 123
• dimensione in byte (size): 100.size -> 4
• operazioni di conversione (to_f, to_s, to_sym): 1.to_s -> "1", 1.to_f ->
1.0
Altri metodi sono ereditati dalle classi Integer e Numeric poiché entrambe le classi derivano
da Integer che a sua volta deriva da Numeric. Abbiamo ad esempio chr, floor, next, to_i,
step e molti altri.
La classe Float oltre ai metodi base visti per Fixnum e a quelli ereditati da Numeric prevede tra
l'altro:
• infinite? che restituisce -1, +1 o nil a seconda che il valore sia pari a meno infinito, più
infinito o un numero finito: (1.0).infinite? -> nil,
(+1.0/0.0).infinite? -> 1
• nan? restituisce true se il valore è un numero non valido secondo gli standard IEEE.
Per ulteriori dettagli su questi e altri metodi consiglio vivamente l'uso di ri sulle classi Fixnum,
Bignum, Integer e Numeric.
viene restituito un intero che è il valore del codice ASCII corrispondente nel nostro caso al carattere
"C". Per riconvertirlo in carattere si utilizza il metodo chr visto prima:
> str[0].chr
=> "C"
Dove 0..3 e 0...3 sono dei range, le due cifre rappresentano gli estremi dell'intervallo da prendere in
considerazione; se si utilizzano i tre punti (...) il secondo estremo viene escluso. Passando invece
come argomenti due interi, ad esempio 0 e 4, estraiamo da str la sottostringa che inizia al carattere
con indice 0 (la prima lettera della prima parola) e che ha lunghezza pari a 4 caratteri. Inserendo
degli indici negativi il conteggio avviene invece dalla fine della stringa:
> str[-6..-1]
=> "Mondo!"
> str[-6,6]
=> "Mondo!"
Utilizzando una stringa come argomento viene restituito il valore nil se questa non è presente e la
stringa stessa in caso contrario:
> str["Hello"]
=> nil
> str["Mondo"]
=> "Mondo"
Di quasi tutti i metodi visti in questo paragrafo ne esiste anche la versione distruttiva che, invece di
restituire una copia della stringa opportunamente modificata, modifica direttamente la stringa
originaria. Torneremo sulle stringhe nel capitolo sulle espressioni regolari.
Blocchi e iteratori
In questo capitolo faremo una panoramica sui blocchi e sugli iteratori, in modo da avere gli
strumenti necessari per affrontare i prossimi capitoli. Anche se sono caratteristiche presenti in altri
linguaggi da lungo tempo, è il Ruby che ha reso l'utilizzo di questi potenti strumenti incredibilmente
semplice.
Blocchi e iteratori
Iniziamo con un paio di definizioni. Un blocco sintatticamente è una porzione di codice compresa
tra le parole chiave do e end oppure tra una coppia di parentesi graffe { e }; convenzionalmente si
utilizza la seconda forma per blocchi che occupano una sola riga. Un iteratore invece è un normale
metodo che accetta come argomento un blocco, ovvero una funzione anonima che viene eseguita
sui parametri passatigli dall'iteratore. È molto più semplice di quello che sembra, vediamo un
semplice esempio per chiarire un po' le cose:
> str = "Ciao Mondo"
> str.each_byte {|c| puts c.chr}
C
i
a
o
M
o
n
d
o
Abbiamo utilizzato l'iteratore each_byte della classe String che passa ogni byte della stringa
come parametro al blocco tra parentesi graffe. Abbiamo anche passato un parametro (c) tra il
metodo e il blocco. La variabile c, locale al blocco, assume quindi di volta in volta il valore passato
dal metodo, nel nostro esempio i singoli caratteri delle stringa.
È possibile passare dei parametri anche all'iteratore come a qualsiasi altro metodo. Ad esempio
l'iteratore each, o each_line, della classe String accetta come argomento il valore del
separatore (\n è il valore di default):
> str.each {|c| puts c}
Ciao Mondo
> str.each(' ') {|c| puts c}
Ciao
Mondo
Vedremo la grande utilità degli iteratori quando parleremo degli array e delle altre strutture dati.
Prima di concludere diamo un altro sguardo ai blocchi e vediamo come vanno chiamati dall'interno
di un metodo. Subito un esempio che poi commenteremo:
def chiama
puts "Sono nel metodo"
yield
end
Per richiamare il blocco abbiamo usato l'istruzione yield, che non fa altro richiamare il blocco
associato al metodo che lo contiene. È possibile utilizzare più volte yield all'interno di un metodo:
def chiama
yield
puts "Sono nel metodo"
yield
end
Apriamo una breve parentesi sul significato di #{espressione} all'interno della stringa passata
a puts. In pratica il codice contenuto all'interno delle parentesi viene eseguito e il risultato
trasformato in stringa, e nel nostro esempio il tutto è stampato.
Infine possiamo creare un blocco contenente delle normali espressioni:
begin
#espressione1
#espressione2
#...
end
Il valore dell'ultima espressione valutata sarà anche il valore del blocco. Come vedremo tra qualche
capitolo begin/end risulta di grande utilità soprattutto nella gestione delle eccezioni.
Array - I
Un array può essere visto come una lista di oggetti non necessariamente dello stesso tipo. Gli array
sono istanze della classe Array e vanno dunque creati nel seguente modo:
arr = Array.new
in entrambi i casi creeremo un array vuoto. Se invece si deve creare un array non vuoto è possibile
passare una lista di valori separati da virgole:
arr = [1, 2, "tre", "IV"]
in questo modo abbiamo creato un array di quattro elementi, due interi e due stringhe. È possibile
chiamare il metodo new anche con uno o due parametri, il primo indica le dimensioni dell'array
mentre il secondo il valore di inizializzazione:
> arr = Array.new(3)
=> [nil, nil, nil]
arr = Array.new(3, 100)
=> [100, 100, 100]
Per le altre forme del metodo new rimando alla documentazione ufficiale.
Per inserire nuovi elementi nell'array o per modificare quelli esistenti si utilizza il metodo []=
> arr = [1, 2, 2]
=> [1, 2, 2]
> arr[2] = 3
=> 3
> arr
=> [1, 2, 3]
Si può anche indicare un indice superiore alle dimensioni dell'array, in tal caso l'array si estende
automaticamente e gli elementi compresi tra il l'indice indicato e l'ultimo elemento dell'array sono
impostate a nil:
> arr[8] = 9
=> 9
> arr
=> [1, 2, 3, nil, nil, nil, nil, nil, 9]
Come indice è possibile indicare due interi che rappresentano rispettivamente l'indice di inizio e il
numero di elementi da sostituire con il valore passato:
> arr = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
> arr[0,2] = ["I", "II"]
=> ["I", "II"]
> arr
=> ["I", "II", 3, 4, 5]
> arr[2,3] = "III"
=> "III"
> arr
=> ["I", "II", "III"]
in questo modo abbiamo creato un array di cinque elementi e poi abbiamo sostituito i primi due
(indice 0 e numero di elementi 2) con i valore "I" e "II". Abbiamo infine sostituito i restanti tre
elementi con il valore "III". Analogamente può essere usato anche un range con ovvio significato,
l'esempio precedente può essere riscritto così:
> arr = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
> arr[0..1] = ["I", "II"]
=> ["I", "II"]
> arr
=> ["I", "II", 3, 4, 5]
> arr[2..4] = "III"
=> "III"
> arr
=> ["I", "II", "III"]
Per il resto, per quel che riguarda i range, valgono le stesse proprietà già viste per le stringhe.
L'accesso agli elementi avviene attraverso il metodo [], o il metodo slice che prevede anche una
forma distruttiva slice! che elimina dall'array gli elementi indicati dall'indice. Come per []=,
anche [], può prendere come argomenti un intero, oppure due interi o infine un range.
Array - II
Un altro metodo per inserire elementi in un array è rappresentato dai metodi << e push che
inseriscono i nuovi valori in fondo all'array che si comporta in questo caso come una pila (stack):
> arr = [1, 2, 3]
=> [1, 2, 3]
> arr << 5 << 6
=> [1, 2, 3, 5, 6]
> arr.push(7,8)
=> [1, 2, 3, 5, 6, 7, 8]
All'opposto il metodo pop invece elimina l'ultimo elemento di un array:
> arr.pop
=> 8
> arr
=> [1, 2, 3, 5, 6, 7]
Un altro metodo per inserire valori in un array già esistente è insert che prende come argomenti
un indice ed uno o più oggetti da inserire nell'array:
> arr = [10, 30, 40]
=> [10, 30, 40]
> arr.insert(1, 20)
=> [10, 20, 30, 40]
è possibile anche indicare due o più oggetti, in questo modo il primo verrà inserito nella posizione
indicata dall'indice e gli altri di seguito:
> arr.insert(4, 50, 60, 70)
=> [10, 20, 30, 40, 50, 60, 70]
Agli array è possibile anche applicare le operazioni +, -, *, &. Il metodo concatenazione (+)
restituisce un nuovo array ottenuto dalla concatenazione di due array:
> a = ["a", "e", "i"]
=> ["a", "e", "i"]
> b = ["o", "u"]
=> ["o", "u"]
> c = a + b
=> ["a", "e", "i", "o", "u"]
Analogamente il metodo differenza restituisce un nuovo array ottenuto dalla differenza di due array:
> a = ["a", "e", "i"]
=> ["a", "e", "i"]
> b = ["a", "u"]
=> ["a", "u"]
> c = a - b
=> ["e", "i"]
Il metodo (*) invece si comporta in modo diverso a seconda dell'argomento; se gli viene passato un
intero n restituisce un array costituito da n copie dell'array originario:
> a = ["a", "e", "i"]
=> ["a", "e", "i"]
> a * 2
=> ["a", "e", "i", "a", "e", "i"]
Se invece all'argomento può essere applicato il metodo to_str, e non è un intero, avremo come
risultato una stringa costituita dagli elementi dell'array intervallati dalla stringa argomento:
> a = ["a", "e", "i"]
=> ["a", "e", "i"]
> a * "..."
=> "a...e...i"
Infine c'è il metodo intersezione (&) che restituisce un nuovo array costituito dagli elementi comuni
dei due array:
> a = ["a", "e", "i"]
=> ["a", "e", "i"]
> b = ["i", "e", "i"]
=> ["i", "e", "i"]
> a & b
=> ["e", "i"]
each_index invece di passare gli elementi come parametri passa gli indici di tali elementi:
> arr.each_index {|indice| puts indice}
0
1
2
=> ["rosso", "bianco", "nero"]
Il metodo collect e il suo sinonimo map, ereditati da Enumerable, invocano il blocco per ogni
elemento dell'array e restituiscono un array costituito dagli elementi modificati dal blocco:
> arr = [1, 2, 3]
=> [1, 2, 3]
> arr.collect{|n| n*3}
=> [3, 6, 9]
delete_if elimina dall'array tutti quegli elementi per il quale la valutazione nel blocco
restituisce valore true. Similmente si comportano select e reject.
Hash
Gli hash, istanze della classe Hash, sono liste di coppie di chiavi e valori. A differenza degli array
invece degli indici numerici, che identificano la posizione nell'array dell'elemento, negli hash
troviamo delle chiavi che possono essere di tipo qualsiasi. In Ruby gli hash sono racchiusi tra
parentesi graffe e ogni coppia e separata dall'altra da una virgola, mentre tra le chiavi e i valori ci
deve essere il simbolo =>
Come gli array anche gli hash possono essere creati in vari modi, innanzitutto scrivendo
direttamente le coppie chiave-valore tra parentesi graffe
> hsh = {"nome" => "Lev", "patronimico" => "Nikolaevic", "cognome" => "Tolstoj"}
=> {"cognome"=>"Tolstoj", "nome"=>"Lev", "patronimico"=>"Nikolaevic"}
tale hash va poi popolato, come abbiamo visto per gli array, con il metodo []= con la differenza che
invece dell'indice va indicata la chiave:
> hsh["nome"] = "Vladimir"
=> "Vladimir"
> hsh["patronimico"] = "Vladimirovic"
=> "Vladimirovic"
> hsh["cognome"] = "Majakovskij"
=> "Majakovskij"
> hsh
=> {"cognome"=>"Majakovskij", "nome"=>"Vladimir", "patronimico"=>"Vladimirovic"}
La stessa sintassi si utilizza per accedere ai valori con il metodo []. Per elencare invece tutte le
chiave e i valori di un hash si utilizzano i metodi keys e values:
> hsh.keys
=> ["cognome", "nome", "patronimico"]
> hsh.values
=> ["Majakovskij", "Vladimir", "Vladimirovic"]
È possibile inoltre indicare un valore di default che viene restituito qualora si utilizza una chiave
non esistente:
> hsh.default = "errore!"
=> "errore!"
> hsh["nazionalità"]
=> "errore!"
Tale valore può essere impostato anche al momento della creazione passando il valore di default
come argomento a new.
Oltre a buona parte degli iteratori che abbiamo visto per gli array, gli hash ne hanno di propri che
permettono di sfruttare al meglio la loro struttura.
Oltre a each, che in questo caso passa al blocco sia la chiave che il valore come parametri,
abbiamo i metodi each_key e each_value che rispettivamente passano la chiave e il valore
al blocco che viene eseguito come al solito su ogni elemento:
> hsh = {"nome" => "Lev", "patronimico" => "Nikolaevic", "cognome" => "Tolstoj"}
> hsh.each {|k, v| puts "#{k}: #{v}"}
cognome: Tolstoj
nome: Lev
patronimico: Nikolaevic
> hsh.each_key {|k| puts k}
cognome
nome
patronimico
> hsh.each_value {|v| puts v}
Tolstoj
Lev
Nikolaevic
Tipico degli hash è anche il metodo has_value? che restituisce true se il valore passato come
argomento è presente nell'hash. Analogamente, ma per le chiavi, opera has_key?. Rimando come
al solito alla documentazione ufficiale per l'elenco completo dei metodi della classe Hash.
If e unless
If
Iniziamo dando uno sguardo a if. Sorvoliamo sul significato di questo costrutto, noto a chiunque
conosca l'abc della programmazione, e passiamo direttamente alla sintassi con un esempio:
if n == 1 then
puts "N vale uno"
elsif n == 2 then
puts "N vale due"
else
puts "N non vale ne uno ne due"
end
È possibile omettere, qualora non necessario, sia il ramo elseif sia il ramo else e arrivare alla
forma base
if espressione then
istruzione
end
è anche possibile sostituire alla parola chiave then i due punti (:), o anche ometterla del tutto se il
nostro if è scritto su più linee. L'esempio di prima diventa dunque:
if n == 1
puts "N vale uno"
elsif n == 2
puts "N vale due"
else
puts "N non vale ne uno ne due"
end
Unless
Opposto all'if abbiamo lo statement unless che esegue le istruzioni associate all'espressione che
risulta falsa:
unless n > 10
puts "N non è maggiore dieci"
else
puts "N è maggiore di dieci"
end
Una forma ancora più ermetica prevede l'uso dell'operatore ? nel seguente modo:
segno = n >= 0 ? "positivo" : "negativo"
se la condizione è vera viene eseguita la prima espressione, se è falsa la seconda. È buona norma
usare questa sintassi solo nei casi in cui non va a scapito della leggibilità, uno dei punti di forza del
Ruby.
Infine come esercizio riscriviamo il metodo rifornimento dell'esempio che abbiamo scritto in
precedenza tenendo contro della capacità massima del serbatoio. Innanzitutto bisogna creare una
variabile per la capacità massima, e poi non ci resta che controllare se la quantità di carburante che
vogliamo immettere più quella già presente nel serbatoio non sia superiore alla capacità stessa. In
caso affermativo eroghiamo solo la quantità di carburante necessaria per un pieno.
Scrivendo il tutto in modo didattico otteniamo il codice seguente:
CAPACITA_MAX_SERBATOIO = 100
In alternativa avremmo potuto scrivere in modo più conciso, su una unica riga, anche in questo
modo:
def rifornimento (quantita)
quantita = (@carburante + quantita) > CAPACITA_MAX_SERBATOIO ?
CAPACITA_MAX_SERBATOIO - @carburante : quantita
@carburante += quantita
puts "Carburante erogato: #{quantita}"
puts "Carburante totale: #{carburante}"
end
Case
In molti casi casi in alternativa a if, e unless, è più comodo e più chiaro utilizzare il costrutto
case. vediamolo in dettaglio. La sintassi classica è la seguente:
case command
when "start"
puts "avvio in corso..."
when "stop"
puts "arresto in corso..."
else
puts "Comandi: start/stop"
end
L'istruzione che segue l'else rappresenta il ramo di default che viene eseguito se tutte le altre
condizioni falliscono. Se la condizione è sulla stessa linea dell'espressione è possibile utilizzare la
parola chiave then o i due punti ottenendo una sintassi più compatta:
case n
when n > 10: puts "N è maggiore di dieci"
when n < 10: puts "N è minore di dieci"
else puts "N vale dieci"
end
A differenza dell'esempio precedente in questo caso abbiamo utilizzato come condizioni delle
espressioni booleane, in alternativa è possibile utilizzare anche delle espressioni regolari, dei range
o delle classi rendendo il case uno strumento incredibilmente potente. Inoltre poiché case ritorna il
valore dell'ultima espressione valutata è possibile assegnare ad una variabile il risultato del
costrutto:
value_n = case n
when n > 10: "N è maggiore di dieci"
when n < 10: "N è minore di dieci"
else "N vale dieci"
end
il ciclo è stato eseguito mentre la lettera ha assunto valori minori, in ordine alfabetico, di "g". Di
natura opposta è until che esegue il ciclo finché l'espressione è falsa, ad esempio:
lettera = "a"
L'output è:
a b c d e f g
In questo caso il ciclo è proseguito finché la lettera non è divenuta maggiore di "g".
For
L'altro ciclo di notevole interesse è il classico for. Riprendendo l'esempio precedente possiamo
scrivere
for n in "a".."g"
print n, " "
end
ottenendo
a b c d e f g
Il ciclo viene eseguito una volta per ogni valore assunto da n. Nel nostro esempio la variabile n
assume i valori del range che ha per estremi le lettere "a" e "g". Oltre al range avremmo potuto
indicare un array, o qualsiasi altro oggetto che risponde al metodo each, e la variabile avrebbe
assunto tutti i valori degli elementi dell'array, uno per ogni ciclo proprio come un iteratore.
Uscire dai cicli
Concludiamo questa porzione della guida illustrando i costrutti che permettono di modificare la
normale esecuzione di un ciclo al verificarsi di particolari condizioni.
break: termina immediatamente il ciclo e l'esecuzione del programma viene ripresa dall'istruzione
immediatamente successiva al ciclo. Ad esempio questo codice
while lettera < "g"
break if lettera == "d"
print lettera, " "
lettera.next!
end
ci darà in output
a b c
Quando num ha assunto valore 3 il next ha terminato l'iterazione corrente, andando alla fine del
ciclo e saltando dunque l'istruzione di stampa, e ha iniziato l'iterazione successiva.
Ereditarietà singola
In questo e nel successivo capitolo vedremo in maggiore dettaglio gli aspetti più interessanti, e utili,
della programmazione OO in Ruby. Completeremo il discorso sulle classi iniziato nel capitolo 8
introducendo in questo capitolo l'ereditarietà singola e nel prossimo l'ereditarietà multipla.
Ricominciamo da dove eravamo rimasti. Alla fine del capitolo 8 avevamo due classi
CarroArmato e Camion:
class CarroArmato
attr_reader :colpi, :carburante
class Camion
attr_reader :posti, :carburante
Anche se scritte in modo corretto le nostre due classi non sono sicuramente scritte nel modo
migliore possibile, abbiamo infatti ignorato i principi base dell'ingegneria del software. Si nota
subito infatti che ci sono delle ripetizioni che possono essere eliminate utilizzando lo strumento
dell'ereditarietà. Quindi un modo migliore di progettare le nostre classi prevede la creazione di una
superclasse che possiede gli attributi e i metodi comuni alle sottoclassi CarroArmato e
Camion. Ad esempio creando la classe Veicolo nel seguente modo:
class Veicolo
attr_reader :carburante
Vediamo cosa è successo. Innanzitutto ora abbiamo una gerarchia di classi che vede in cima la
classe-padre, o superclasse, Veicolo che ha due classi-figlie, o sottoclassi, Camion e
CarroArmato che ne ereditano il metodo rifornimento e in parte il metodo initialize. In
realtà le nostre sottoclassi specializzano il metodo initialize di Veicolo: per la variabile
d'istanza comune, @carburante, viene chiamato il metodo initialize della superclasse,
attraverso il costrutto super, mentre le altre variabili sono gestite direttamente dalle sottoclassi. In
definitiva super non fa altro che richiamare il metodo con lo stesso nome contenuto nella
superclasse.
Ereditarietà multipla
Ruby, di per sé, non permette l'ereditarietà multipla: ogni classe non può avere più di una
superclasse. È possibile però aggirare questo "problema", che a detta di molti problema non è,
attraverso l'utilizzo dei moduli e il meccanismo del mixin.
Oltre al classico utilizzo per la creazione di namespace, i moduli possono essere utilizzati per
implementare una sorta di ereditarietà multipla. Vediamo innanzitutto cosa si intende per modulo e
come si definisce.
I moduli sono dei contenitori di metodi, costanti e classi e, alla stessa maniera delle classi, devono
avere un nome che inizia per lettera maiuscola. Sono definiti dalla parola chiave module; un
esempio è il seguente:
module Net
class HTTP
def HTTP.get_print(uri_or_host, path = nil, port = nil)
...
end
end
end
Per poter accedere alle classi del modulo occorre importalo con l'istruzione require e poi fare
riferimento alle classi con il nome esteso. Ad esempio se si vuole utilizzare il metodo get_print
della classe HTTP del modulo Net occorre scrivere:
require 'net/http'
Net::HTTP.get_print 'www.google.com', '/index.html'
La creazione di un namespace permette sia di evitare problemi con i nomi, sia di rendere il codice
più coerente e pulito. Oltre a ciò, come anticipato prima, è possibile utilizzare i moduli in maniera
meno ortodossa ma molto efficace. Per ovviare alla mancanza dell'ereditarietà multipla basta
racchiudere in un modulo i metodi che vogliamo "far ereditare" alla nostra classe e quindi
includerlo con l'istruzione include. Di conseguenza possiamo utilizzare tutti i metodi del modulo
dall'interno della nostra classe come dei normali metodi che vengono mescolati, mixed in appunto, a
quelli definiti dalla classe e a quelli ereditati dalla superclasse.
Un esempio ci chiarirà tutto. Creiamo un semplice modulo con un solo metodo che non fa altro che
stampare una frase a video:
module Inutile
def hello
"Saluti da #{self.class.name}"
end
end
class Tokyo
include Inutile
end
Istanziando una nuova classe possiamo chiamare il metodo hello ereditato dal modulo ottenendo:
> tk = Tokyo.new
> tk.hello
=> "Saluti da Tokyo"
In questo modo è possibile aggiungere alle classi una gran quantità di metodi senza nessuna fatica.
Ad esempio possiamo dotare le nostre classi dei metodi di confronto includendo Comparable, o
anche aggiungere dei metodi di ricerca e ordinamento includendo Enumerable e così via.
fatto questo tutte le operazioni vanno fatte sull'oggetto mio_file di tipo File. Alla fine dopo
averlo opportunamente processato il file va chiuso con:
mio_file = File.close("test.txt", "r+")
File.new può prendere da uno a tre argomenti, il primo rappresenta il nome del file da aprire, il
secondo la modalità di apertura e il terzo i permessi da associare al file. La modalità di apertura può
essere espressa sia come stringa sia come "or" di flag. Tra le principali flag, che sono costanti della
classe File, troviamo:
• APPEND: apertura in modalità append
• CREAT: se il file da aprire non esiste viene creato
• RDONLY: apertura a sola lettura
• RDWR: apertura per lettura e scrittura
• TRUNC: apre il file e lo azzera se già esiste
• WRONLY: apertura a sola lettura
Analogamente alle flag è possibile usare le stringhe:
• r: apertura a sola lettura
• r+: apertura per lettura e scrittura dall'inizio del file
• w: apertura a sola scrittura, se il file non esiste ne viene creato uno nuovo, altrimenti viene
azzerato
• w+: apertura per lettura e scrittura, se il file non esiste ne viene creato uno nuovo, altrimenti
viene azzerato
• a: apertura a sola scrittura dalla fine del file, se il file non esiste viene creato
• a+: apertura per lettura e scrittura dalla fine del file, se il file non esiste viene creato
• b: va aggiunto ad uno dei precedenti e permette la gestione in modalità binaria, esiste solo
per sistemi Windows.
I permessi, come terzo argomento, vanno indicati, nei sistemi Unix-like, nella classica forma
Owner-Group-Other.
Oltre alla modalità classica di apertura, con la coppia new/close, il Ruby permette di aprire un file
anche con il metodo open. A differenza di new a open può essere associato un blocco al quale
viene passato il file appena aperto come parametro:
File.open("test.txt", "r+") do |mio_file|
...
end
Utilizzando open non c'è più bisogno di chiamare al termine delle operazioni close poiché il
file viene chiuso automaticamente all'uscita dal blocco; open può anche essere invocato senza
blocco e in tal caso è solo un sinonimo di new e valgono per esso tutte le considerazioni fatte in
precedenza.
Lettura e scrittura
Dopo averlo aperto le operazioni base sui file sono la lettura e la scrittura. Per leggere un file una
linea alla volta si può utilizzare il metodo gets ereditato da IO:
while linea = mio_file.gets
puts linea
end
in questo modo con un ciclo while non facciamo altro che stampare a video tutte le righe del file.
In alternativa è possibile usare anche gli iteratori che insieme a open con i blocchi rendono le
operazioni di lettura molto agevoli. Tra gli iteratori troviamo:
• each_byte che chiama il blocco per ogni byte
• each_line, e il suo sinonimo each, chiamano il blocco per ogni riga
Ad esempio supponendo che il nostro file test.txt abbia il seguente contenuto:
Vento invernale
Un monaco scinto
Cammina nel bosco
in questo modo trasformiamo il byte letto, un Fixnum, in carattere con il metodo chr e poi lo
stampiamo seguito da uno spazio. L'output sarà:
V e n t o i n v e r n a l e
U n m o n a c o s c i n t o
C a m m i n a n e l b o s c o
Gli iteratori each e each_line, come detto, permettono invece di iterare su ogni linea del file.
Un esempio è:
File.open("test.txt") do |f|
f.each {|l| puts "#{f.lineno} #{l}"}
end
dove lineno è un metodo della classe IO che restituisce il numero di linea corrente. L'output sarà
dunque:
1 Vento invernale
2 Un monaco scinto
3 Cammina nel bosco
Infine un altro iteratore, che è allo stesso tempo un altro modo per aprire un file, è foreach che
quando è invocato apre il file passatogli come argomento, chiama il blocco per ogni linea e alla fine
chiude il file automaticamente:
IO.foreach("test.txt") {|linea| puts linea}
Per scrivere su di un file si utilizza il metodo write che prende come argomento la stringa da
scrivere sul file:
File.open "test.txt", "w" do |mio_file|
mio_file.write "Ciao file"
end
write restituisce il numero di byte scritti. In alternativa si può utilizzare anche l'operatore << o il
metodo puts. L'esempio precedente diventa:
File.open "test.txt", "w" do |mio_file|
mio_file << "Ciao file"
end
oppure
File.open "test.txt", "w" do |mio_file|
mio_file.puts "Ciao file"
end
Le directory
Prima di concludere questo capitolo, per completezza, diamo uno sguardo anche alle directory. La
classe per la gestione delle directory è Dir e anche in questa classe abbiamo i metodi new, open
e close con lo stesso significato visto per i file. Ad esempio creiamo, apriamo e chiudiamo una
directory in questo modo:
Dir.mkdir("test")
test_dir = Dir.new("test")
test_dir.close
oppure con open, senza bisogno di chiamare close alla fine delle operazioni:
Dir.open("test") do |d|
...
end
Dove Dir.mkdir crea una nuova directory, è possibile passargli come secondo argomento anche i
relativi permessi. Per il resto rimando alla documentazione ufficiale per un elenco completo di tutti i
metodi.
Le espressioni regolari
Le espressioni regolari sono uno strumento fondamentale per la gestione del testo, e uno dei
linguaggi che ha fatto di questo strumento un punto di forza è sicuramente il Perl. Il Ruby, che nelle
idee di Matz nasce come suo naturale erede, non poteva non avere un supporto alle espressioni
regolari altrettanto potente.
Brevemente, rimandando a testi appositi una trattazione più approfondita, possiamo dire che le
espressioni regolari vengono utilizzate per controllare se una stringa verifica un certo schema
(pattern). O in altre parole, le espressioni regolari, forniscono dei modelli per ricercare all'interno di
un testo non solo utilizzando espressioni letterali ma anche particolari identificatori che
rappresentano delle determinate classi di caratteri.
In Ruby sono oggetti della classe Regexp che possono essere creati, come gli Array e gli Hash, in
diversi modi. Si può utilizzare il metodo new di Regexp:
expr = Regexp.new('')
passando come argomento l'espressione regolare vera e propria, oppure racchiudendola tra due /
('slash'):
expr = //
Una volta creata la nostra espressione regolare possiamo confrontarla con qualsiasi stringa
utilizzando il metodo match della classe Regexp oppure l'operatore =~ e il suo negato !~. Ad
esempio:
> er = Regexp.new('\s\w+')
=> /\s\w+/
> stringa = "Ciao mondo"
=> "Ciao mondo"
> stringa =~ er
=> 4
> $&m
=> " mondo"
> $`
=> "Ciao"
=~ restituisce la posizione del primo carattere che verifica l'espressione regolare mentre le variabili
speciali $& e $` rappresentano rispettivamente la parte di stringa che verifica il pattern e la parte
che non lo verifica.
Come già detto, all'interno di una espressione regolare oltre ai normali caratteri è possibile usare
delle sequenze che rappresentano delle determinate classi, in Tabella1 c'è un elenco completo. In
alternativa è possibile creare delle classi racchiudendo i caratteri tra parentesi quadre, ad esempio
[A-Za-z] sta a indicare tutte le lettere dalla a alla z sia maiuscole che minuscole.
Tabella 1: classi di caratteri
. un carattere qualsiasi
\w lettera o numero, come [0-9A-Za-z]
\W il contrario di \w, ne' lettera ne' cifra
spazio (spazio vero e proprio, tabulazione e carattere di fine riga), come
\s
[\t\n\r\f]
\S carattere non spazio
\d cifra numerica, come [0-9]
\D il contrario di \d, carattere non numerico
\b backspace se si trova in una specifica di intervallo
\b limite di parola
\B non limite di parola
Oltre ai caratteri in una espressione regolare è possibile anche utilizzare due particolari elementi, ^
('accento circonflesso') e $ ('dollaro'), che indicano una precisa posizione del testo cercato. ^ va
usato se i caratteri cercati si devono trovare all'inizio del testo, $ se si devono trovare alla fine. Ci
sono quindi i simboli quantificativi:
Tabella 2: i simboli di quantità
* zero o più ripetizioni del carattere precedente
+ una o più ripetizioni del carattere precedente
{m,n} almeno m e massimo n ripetizioni del carattere precedente
? al massimo una ripetizione del precedente; lo stesso che {0,1}
Abbiamo poi | ('pipe') che rappresenta il meccanismo dell'alternanza che permette di definire più
alternative. Ad esempio /http|ftp|smtp/ permette di trovare una delle corrispondenze
indicate nell'espressione. Infine, come è consueto, è possibile catturare una parte della stringa
verificata in modo da utilizzarla sia all'interno del pattern stesso, riferendoci ad essa con \1, \2 e
così via, sia all'esterno utilizzando le variabili speciali $1, $2 ecc. Ad esempio volendo catturare la
parola "mondo" dell'esempio precedente dobbiamo scrivere:
> er = Regexp.new('\s(\w+)')
=> /\s(\w+)/
> stringa = "Ciao mondo"
=> "Ciao mondo"
> stringa =~ er
=> 4
> $1
=> "mondo"
Allo stesso modo si utilizzano con index che restituisce l'indice della prima occorrenza della
sottostringa espressa dal pattern:
> str.index(/[aeiou]/)
=> 1
Possiamo usare le espressioni regolari anche come argomento dei metodi sub e gsub:
> str.gsub(/([aeiou])/, '(\1)' )
=> "C(i)(a)(o) m(o)nd(o)"
> str.gsub(/\s(\w+)/){|c| c.upcase}
=> "Ciao MONDO"
Anche split può essere utilizzato passando come argomento un'espressione regolare:
> str.split(/\s/)
=> ["Ciao", "mondo"]
Interessante è infine l'utilizzo delle regexp con scan che itera attraverso la stringa e inserisce
tutti i risultati che verificano il pattern all'interno di un array:
> str.scan(/\w+/)
=> ["Ciao", "mondo"]
Da questi esempi è lampante la notevole versatilità che le espressioni regolari aggiungono ai metodi
della classe String.
Le eccezioni
Concludiamo con questo e con il successivo capitolo la prima parte della nostra panoramica del
Ruby trattando la gestione delle eccezioni, dal prossimo ci occuperemo di questioni accessorie al
linguaggio vero e proprio.
Come tutti i linguaggi moderni, anche il Ruby, possiede il meccanismo delle eccezioni,
fondamentale nella gestione degli errori. Un'eccezione va sollevata utilizzando il metodo raise
del modulo Kernel. La sintassi completa prevede tre argomenti:
raise(exception [, string [, array]])
Il primo argomento è il nome di una classe della gerarchia Exception (vedi lista più avanti) e
rappresenta il tipo di eccezione da sollevare. Il secondo argomento è un messaggio che verrà
associato all'eccezione, mentre il terzo è un array con il callback dell'eccezione. Chiamato senza
nessun argomento raise solleverà un eccezione di tipo RuntimeError, oppure se definita
l'eccezione contenuta in $!. Ad esempio:
raise "Errore nell'apertura del file"
raise ArgumentError, "Tipo di argomento non gestito"
raise ArgumentError, "Tipo di argomento non gestito", caller
e in caso di eccezione LoadError vedremo sullo schermo la scritta "L'errore è: no such file to
load -- gmailer". All'interno del corpo di rescue si può inserire un'istruzione retry con la
funzione di rieseguire il blocco dall'inizio, ovviamente prima di retry vanno eseguite delle
istruzioni atte a risolvere il problema riscontrato. Infine c'è la clausola ensure utile quando si
devono eseguire delle operazioni all'uscita da un blocco begin/end. Un classico esempio è quello
della gestione dei file:
file = File.open("test.txt")
begin
# operazioni varie sul file
rescue
# gestione degli errori
ensure
file.close unless file.nil?
end
Nel blocco principale vengono eseguite le normali operazioni sul file aperto, poi c'è il blocco
rescue per la gestione degli errori e infine nel blocco ensure ci sono le istruzioni per chiudere
il file. In questo modo siamo sicuri che all'uscita dal blocco il file verrà chiuso.
Oltre che all'interno dei blocchi begin/end, è possibile gestire le eccezioni anche all'interno dei
normali metodi, il cui corpo è implicitamente un blocco begin/end:
def nome_metodo
# istruzioni varie
rescue
# gestione degli errori
end
All'inizio va inserito l'apposito tag che segna l'inizio dei commenti utilizzati da RDoc.
Si possono creare vari effetti, ad esempio le parole in *grassetto* vanno inserite tra due asterischi,
quelle in _corsivo_ vanno tra due underscore e quelle +monospazio+ vanno scritte tra due più
(+).
Per creare frasi in grassetto, corsivo e con carattere monospace vanno utilizzati rispettivamente i tag
b, em e tt con le relative chiusure:
<b>Questa è una frase scritta in grassetto.</b>
<em>Quest'altra invece è in corsivo,</em> <tt>ed infine una scritta con
carattere monospazio.</tt>
a. primo elemento
b. secondo elemento
oppure
nome:: Lev
cognome:: Tolstoj
Per generare la documentazione dall'esempio appena visto salviamolo in un file che chiamiamo
esempio_rdoc.rb ed eseguiamo RDoc:
$ rdoc esempio_rdoc.rb
esempio_rdoc.rb:
Generating HTML...
Files: 1
Classes: 0
Modules: 0
Methods: 0
Elapsed: 0.108s
Questo comando creerà una directory doc contenente la documentazione relativa ai nostri sorgenti.
In Figura 3 uno stralcio dell'output prodotto.
Figura 3: esempio di output di Rdoc
Commentiamole inserendo una descrizione generica per ogni classe, e poi descriviamo tutti gli
attributi e tutti i metodi; il risultato finale sarà:
# Implementazione di un veicolo generico utilizzato
# come superclasse per tutti gli altri veicoli.
class Veicolo
Salviamo tutto ed eseguiamo RDoc che, se chiamato senza argomenti, genera la documentazione
per tutti i sorgenti Ruby e C (eventuali estensioni di Ruby) presenti nella directory corrente e nelle
sue sotto-directory:
$ rdoc veicolo.rb
veicolo.rb: c..c.c.
Generating HTML...
Files: 1
Classes: 3
Modules: 0
Methods: 4
Elapsed: 0.176s
In Figura 4 vediamo la pagina di documentazione della classe Veicolo con in evidenza il pop-up
del metodo rifornimento. I pop-up vengono chiamati attraverso i link collegati ai nomi dei
metodi e mostrano i relativi stralci di codice.
Figura 4: documentazione con Rdoc
In Figura 5 vediamo invece il codice come è generato da RDoc chiamato con le opzioni -NS che
vedremo dopo.
Figura 5: Documentazione con il comando Rdoc -NS
Rdoc: documentazione automatica
Oltre alle caratteristiche di formattazione viste prima RDoc mette a disposizione una gran quantità
di direttive molto avanzate. Rimando dunque alla documentazione ufficiale per maggiori dettagli e
approfondimenti.
Tra le tante cose, RDoc viene incontro anche a tutti quegli sviluppatori che odiano scrivere la
documentazione, fosse anche solo l'help in line. Infatti RDoc riesce a tirare fuori della
documentazione utile in HTML anche da sorgenti privi di qualsiasi commento. Facciamo una
prova, riprendiamo il nostro esempio nella versione senza commenti e vediamo RDoc cosa ci tira
fuori.
class Veicolo
attr_reader :carburante
veicolo.rb: c..c.c.
Generating HTML...
Files: 1
Classes: 3
Modules: 0
Methods: 4
Elapsed: 0.123s
In Figura 6 una pagina generata da RDoc, è il massimo che si può ottenere senza scrivere una riga
di documentazione, ed è comunque utile per capire come sono strutturate le classi.
Figura 6: La documentazione automatica
Rdoc: le opzioni
Molti aspetti possono essere gestiti utilizzando le opzioni del tool rdoc, ecco le principali:
• --diagram, -d viene generato un semplice diagramma che mostra i moduli e le classi
• --inline-source, -S il codice sorgente dei moduli viene mostrato inline invece che attraverso
i pop-up
• --line-numbers, -N viene mostrato il numero di linea nei sorgenti
• --one-file, -1 tutto l'output viene inserito in un unico file
• --fmt, -f format name imposta il tipo di file generato, è possibile produrre file HTML, XML
e CHM
l'elenco completo delle opzioni è visualizzabile chiamando rdoc con l'opzione --help. RDoc è
capace di generare anche documentazione leggibile dal tool ri utilizzando le seguenti opzioni
• --ri, -r viene generata documentazione, leggibile da ri, che viene salvata nella directory
.rdoc nella directory home a meno che non venga indicato un altro percorso con l'opzione --
op oppure -o.
• --ri-site, -R i file generati vengono salvati in una directory globale, ad esempio
/usr/share/ri/1.8/site/ rendendoli accessibili a tutti gli utenti.
• --ri-system, -Y i file generati vengono salvati in una directory di sistema, ad esempio
/usr/share/ri/1.8/system/; opzione da utilizzare in fase di installazione.
Ad esempio:
$ rdoc --ri veicolo.rb
veicolo.rb: c..c.c.
Generating RI...
Files: 1
Classes: 3
Modules: 0
Methods: 4
Elapsed: 0.181s
$ ls .rdoc/
Camion/ CarroArmato/ created.rid Veicolo/
$ ri Veicolo#rifornimento
--------------------------------------------------- Veicolo#rifornimento
rifornimento(quantita)
------------------------------------------------------------------------
rifornisce il veicolo di carburante
Introduzione a RubyGems
Dopo aver scritto la nostra applicazione, dopo averla opportunamente commentata e dopo aver
generato la documentazione non ci resta che rendere l'installazione il più agevole possibile per tutti i
probabili utenti. Gli strumenti che Ruby mette a disposizione per la gestione dei pacchetti sono
essenzialmente due, diversi per consistenza e funzionalità, ovvero il framework RubyGems e la
libreria setup. In questo capitolo ci occuperemo di RubyGems mentre nel prossimo daremo uno
sguardo al classico setup.rb.
RubyGems è un framework per la gestione automatica dei pacchetti che si occupa di tutti gli aspetti
riguardanti la pacchettizzazione: dall'installazione, all'aggiornamento e alla distribuzione. Di seguito
vedremo come va utilizzato per installare nuove librerie, e impareremo a creare i pacchetti gem per
le nostre applicazioni.
RubyGems non fa parte dei programmi distribuiti direttamente con Ruby ma va installato a parte. I
sorgenti sono disponibili sul sito ufficiale http://rubygems.rubyforge.org e va installato con il
classico:
$ ruby setup.rb
Cominciamo subito a capire come si utilizza vedendolo all'opera. Eseguiamolo senza nessuna
opzione per vedere una schermata di aiuto:
$ gem
Usage:
gem -h/--help
gem -v/--version
gem command [arguments...] [options...]
Examples:
gem install rake
gem list --local
gem build package.gemspec
gem help install
Further help:
gem help commands list all 'gem' commands
gem help examples show some examples of usage
gem help <COMMAND> show help on COMMAND
(e.g. 'gem help install')
Further information:
http://rubygems.rubyforge.org
I comandi di RubyGems
Vediamo ora in dettaglio i principali comandi con degli esempi illustrativi. Cominciamo con quello
più utile e maggiormente utilizzato: install. Per installare una nuova gemma basta scrivere,
avendo i giusti permessi, semplicemente:
$ gem install <nome_pacchetto_da_installare>
ad esempio
$ gem install gmailer
Successfully installed gmailer-0.1.5
Per poter utilizzare le librerie, installate con RubyGems, nei nostri sorgenti bisogna indicare
esplicitamente che è una gemma. Per fare questo occorre caricare prima rubygems e poi
chiamare gem e il normale require. Gem in pratica serve solo quando si vuole indicare a
require di caricare una particolare versione della libreria. Ad esempio:
require 'rubygems'
gem 'mysql', '=2.7'
require 'mysql'
oppure, dato che rubygems carica l'ultima versione disponibile, spesso può bastare un solo:
require 'rubygems'
require 'mysql'
ovvero eseguendo sempre ruby con l'opzione rubygems, possiamo richiamare le librerie all'interno
dei programmi senza dover specificare alcunché:
require 'mysql'
actionmailer (1.3.1)
Service layer for easy email delivery and testing.
actionpack (1.13.1)
Web-flow and rendering framework putting the VC in MVC.
actionwebservice (1.2.1)
Web service support for Action Pack.
Indicando l'opzione --remote è possibile elencare tutte le gemme presenti nel repository remoto,
che se non specificato diversamente è quello ufficiale su rubyforge.org. Possiamo inoltre specificare
come deve iniziare il nome del pacchetto passando una stringa come parametro e ottenendo l'elenco
parziale voluto:
$ gem list --remote gmail
gmailer (0.1.5, 0.1.4, 0.1.3, 0.1.2, 0.1.1, 0.1.0, 0.0.9, 0.0.8, 0.0.7, 0.0.6,
0.0.5)
An class interface of the Google's webmail service
Utilizzando search invece la ricerca avviene su tutto il nome del pacchetto. RubyGems permette,
come detto, anche di aggiornare i pacchetti installati con il comando update seguito dal nome
della gemma, se non viene indicato nessun parametro verranno aggiornati tutti i pacchetti installati
con gem. Le opzioni di update sono a grandi linee le stesse di install.
pescandoli nelle directory bin, lib e test ed escludendo i file relativi al repository e la
documentazione rdoc. In alternativa, in modo più semplice, avremmo potuto scrivere qualcosa tipo
spec.files = Dir['lib/**/*.rb'] + Dir['bin/*']
raccattando tutto quello presente nelle directory lib e bin. E per escludere alcuni tipi di file
l'alternativa è:
spec.files.reject! { |f| f.include? "RDoc" }
Seguono poi altre opzioni che riguardano i test, la documentazione e le dipendenze. L'elenco
completo è visualizzabile sull'homepage del progetto.
Fatto questo non ci resta che salvare il file, ad esempio come Provagem.gemspec, ed eseguire gem
col comando build:
$ gem build ProvaGem.gemspec
Successfully built RubyGem
Name: ProvaGem
Version: 1.0.0
File: ProvaGem-1.0.0.gem
Dopo questo comando nella directory corrente troveremo un pacchetto gem contenente tutto quello
che serve per l'installazione della nostra libreria.
l'ultimo passo, che installa i file sul disco, va fatto con i giusti permessi.
Ogni aspetto del processo di installazione può essere gestito attraverso gli hook che altro non sono
che dei file con un determinato nome posizionati nel giusto posto. Per permettere a setub.rb di
operare nel migliore modo possibile occorre dunque sistemare i sorgenti seguendo uno schema di
directory preciso. Una possibile soluzione, simile a quella già vista nel capitolo precedente, è
riportata di seguito. Nella directory radice del progetto si creano le seguenti directory:
lib/ contiene le applicazioni, e le librerie, Ruby vere e proprie
ext/ contiene eventuali estensioni Ruby scritte in C
bin/ contiene le utility necessarie in fase di installazione
data/ contiene i file dati che accompagnano l'applicazione
conf/ contiene tutti i file di configurazione
man/ contiene le pagine del manuale
test/ contiene i test
e nella directory radice, oltre ai soliti file di istruzioni, di licenza e changelog, avremo anche
setup.rb da utilizzare per l'installazione. Utilizzare setup.rb come installer è molto semplice, basta
solo copiare il file setup.rb, reperibile sul sito http://i.loveruby.net/en/projects/setup/, nella directory
radice e la disposizione dei file nelle giuste directory farà il resto.
dove --quiet è un opzione globale e --prefix è un opzione del task config. Tra le principali opzioni
globali troviamo:
• -q,--quiet non vengono mostrati i messaggi di output
• --verbose vengono mostrati i messaggi di output in modo esteso
• -h,--help viene mostrata una schermata di aiuto
• -v,--version viene mostrata la versione
• --copyright vengono mostrate le informazioni sul copyright
Ecco invece alcune delle opzioni accettate dai task config e all:
• --installdirs serve ad impostare la directory d'installazione
• --prefix serve ad impostare il prefisso per il percorso d'installazione
• --libruby serve ad impostare la directory contente le librerie
• --without-ext non vengono copilate e installate le estensioni
Per un elenco completo rimando alla documentazione presente sul sito ufficiale del progetto.
La struttura
Concludiamo la prima parte della guida a Ruby con l'implementazione, illustrata passo dopo passo,
di una applicazione completa. Nei prossimi tre capitoli scriveremo un programma da linea di
comando per la catalogazione dei CD. Doteremo la nostra applicazione solo delle principali
funzionalità: inserimento di un nuovo CD, cancellazione di un CD dalla lista, visualizzazione della
lista completa. Ecco come apparirà la nostra applicazione:
Ongaku menu:
(a) aggiungi CD (e) elimina CD
(l) visualizza lista (q) esci da 0ngaku
scegli:
La nostra applicazione, che chiameremo, per omaggiare Matz, Ongaku (musica in giapponese), sarà
composta sia da classi che rappresentano i dati veri e propri da gestire sia da classi che vanno a
costruire l'interfaccia. Strutturiamo dunque il tutto nel modo più chiaro possibile. Seguendo lo
schema visto prima nei capitoli sulla pacchettizzazione creiamo una directory base e all'interno di
questa creiamo tutto il necessario, nel nostro caso le directory doc, lib e man.
I file dell'applicazione vanno messi in lib/ e precisamente avremo uno schema simile (Lo stesso
albero di directory visualizzato all'interno di FreeRIDE è in Figura7):
~/Ongaku/lib$ tree
.
|-- ongaku
| |-- cd.rb
| |-- ui
| | |-- gui.rb
| | |-- text.rb
| | `-- web.rb
| `-- ui.rb
`-- ongaku.rb
La classe Cd
Dopo queste premesse iniziamo a scrivere qualche riga di codice. Dato che Ongaku catalogherà CD
cominciamo con lo scrivere proprio la classe Cd. Nella nostra applicazione useremo il modulo
come namespace, quindi racchiuderemo tutte le nostre classi in
module Ongaku
...
end
Per ogni Cd inseriremo solo le informazioni riguardanti il titolo, l'autore e l'anno di pubblicazione.
Come abbiamo visto nel capitolo 8, creiamo gli attributi ai quali assegneremo i valori passati al
momento della creazione di un nuovo oggetto Cd:
attr_accessor :titolo, :autore, :anno
Poiché tra le funzionalità c'è anche la visualizzazione della nostra lista, che sarà un array ordinato di
oggetti Cd, dobbiamo creare il metodo to_s che viene chiamato automaticamente da puts e il
metodo <=> per permettere l'ordinamento dell'array. Il metodo per la stampa degli oggetti Cd è
banalmente:
def to_s
" #{self.autore} - #{self.titolo} (#{self.anno})"
end
Non facciamo altro che produrre una stringa contenente i campi del Cd con un minimo di
formattazione. In questo modo un semplice
puts cd
produrrà l'output
Fursaxa - Lepidoptera (2006)
Per quel che riguarda l'ordinamento dobbiamo solo includere il modulo Comparable (capitolo 11) e
scrivere il metodo <=>. In pratica Comparable ci evita di scrivere tutti i metodi di comparazione
basando il proprio funzionamento solo sull'operatore <=>. Avremo quindi:
include Comparable
def <=>(altro)
self.autore <=> altro.autore
end
def to_s
" #{self.autore} - #{self.titolo} (#{self.anno})"
end
def <=>(altro)
self.autore <=> altro.autore
end
end
end
La classe Applicazione
Vediamo ora il punto d'ingresso di Ongaku costituito dalla classe Applicazione contenuta in
ongaku.rb. Innanzitutto includiamo la libreria GetoptLong, che useremo per la gestione delle
opzioni, e il file ongaku/ui che contiene l'implementazione delle varie interfacce:
require 'getoptlong'
require 'ongaku/ui'
Impostiamo poi alcune costanti che useremo nel corso del programma:
NOME = "0ngaku"
VERSIONE = "0.0.1"
DESCRIZIONE = "Ongaku è un semplice catalogatore di CD"
NOMEFILE = 'cd.yaml'
Dove "cd.yaml" è il nome del file che conterrà la lista dei nostri CD, lo vedremo in dettaglio nel
prossimo capitolo. Svolte queste operazioni accessorie scriviamo la classe Applicazione che come
detto si occuperà di gestire le opzioni da linea di comando e di chiamare il metodo main del modulo
Ui passandogli il tipo scelto da riga di comando:
def initialize
opzioni
Ongaku::Ui.main(@ui)
end
Il primo metodo definisce quali opzioni sono consentite, utilizzando un oggetto GetoptLong, e
precisamente --ui (oppure analogamente -i) che prende un argomento e --help (oppure -h) che
non richiede argomenti. A GetoptLong vanno passati degli array contenenti le stringhe che
rappresentano le opzioni ed un flag per indicare se all'opzione va associato un argomento. Oltre ai
flag REQUIRED_ARGUMENT e NO_ARGUMENT è possibile usare anche OPTIONAL_ARGUMENT.
Dopodiché per ogni opzione passata viene invocato il metodo gestione_opzioni che con un
case sul tipo di opzione imposta alcuni valori oppure chiama semplicemente il metodo aiuto. Il
metodo aiuto visualizza solo alcune righe che mostrano le opzioni accettate ed esce:
def aiuto
puts DESCRIZIONE
puts "#{$0} -i [text|gui|web]"
exit
end
module Ongaku
NOME = "0ngaku"
VERSIONE = "0.0.1"
DESCRIZIONE = "Ongaku è un semplice catalogatore di CD"
NOMEFILE = 'cd.yaml'
class Applicazione
def initialize
opzioni
Ongaku::Ui.main(@ui)
end
def opzioni
opzioni = GetoptLong.new(
["--ui", "-i", GetoptLong::OPTIONAL_ARGUMENT],
["--help", "-h", GetoptLong::NO_ARGUMENT]
)
opzioni.each { |opt, val| gestione_opzioni(opt, val) }
end
Ongaku::Applicazione.new
Interfaccia e YAML
Nella classe Applicazione abbiamo messo Ongaku::Ui.main(@ui) che non fa altro che
chiamare il metodo main del modulo Ui passando come parametro il tipo di interfaccia scelta. Per
salvare i dati su disco utilizzeremo il YAML (potete leggere una nostra breve introduzione a
YAML), ma avremmo potuto usare qualsiasi altro formato, quello che ci interessa è che i dati
presenti nel file cd.yaml saranno caricati all'avvio dell'applicazione in un Array di Cd; dopodiché
tutte le operazioni avverranno su tale array e solo alla chiusura dell'applicazione aggiorneremo,
eventualmente, il file yaml contenente la lista. Vediamo i dettagli.
Come al solito per prima cosa includiamo le librerie e i file necessari:
require 'yaml'
require 'ongaku/ui/text'
require 'ongaku/ui/gui'
require 'ongaku/ui/web'
require 'ongaku/cd'
Creiamo quindi il modulo Ui contenente i principali metodi per la manipolazione dei dati. Il
metodo main chiamato da Applicazione.new() si occupa di caricare il file contenente la
lista e di avviare l'interfaccia selezionata dall'utente in fase di avvio.
def self.main(ui)
@modifica = false
$archivio = Array.new()
Ui.carica_cd
case ui
when 'text'
interfaccia = Ongaku::Text.new
when 'gui'
interfaccia = Ongaku::Gui.new
when 'web'
interfaccia = Ongaku::Web.new
else
puts "Interfaccia non implementata"
exit
end
interfaccia.avvia
end
Analogamente in chiusura l'array $archivio viene riscritto, un oggetto per volta, sul file con
YAML::dump. La variabile @modifica tiene conto delle modifiche all'array, se ce ne sono state
vale true, in caso contrario false.
def self.salva_cd
if @modifica then
open(NOMEFILE, 'w') do |file|
$archivio.each{|cd| YAML::dump(cd, file)}
end
end
end
Anche elimina_cd è molto banale, e non fa altro che eliminare l'elemento dell'array alla
posizione passatagli come argomento.
def self.elimina_cd(index)
@modifica = true
$archivio.slice!(index)
end
L'interfaccia testuale
Come detto prima, dato che in questa prima parte della guida non abbiamo trattato nessuna GUI,
implementeremo solo l'interfaccia testuale. La nostra classe si occuperà di mostrare a video un
menù dei comandi minimale e quindi in base al comando ricevuto raccogliere, o mostrare, i dati
necessari. La classe che implementa tutto questo è Ongaku::Text nel file ongaku/ui/text.rb. Nel
metodo Ui.main le interfacce vengono eseguite con l'istruzione avvia che in questo caso sarà una
cosa del genere:
def avvia
stampa_menu
comando = gets.chomp
case comando
when 'a'
aggiungi
when 'e'
elimina
when 'l'
elenca
when 'q'
Ongaku::Ui.salva_cd
puts "Grazie per aver utilizzato #{NOME}!"
exit
else
puts "Comando inesistente: #{comando}"
end
avvia
end
Innanzitutto viene stampato il menu e poi viene catturato il comando digitato dall'utente, ed in base
a quest'ultimo viene eseguito il metodo associato. Il metodo stampa_menu non fa altro che
stampare a video delle stringhe che mostrano quali comandi sono possibili:
def stampa_menu
puts SEPARATORE
puts "Ongaku menu:"
puts " (a) aggiungi CD (e) elimina CD "
puts " (l) visualizza lista (q) esci da #{NOME}"
print "scegli: "
end
dove SEPARATORE è una stringa costante che vale 45 caratteri '='. Il metodo aggiungi invece
colleziona i dati necessari alla creazione di un nuovo oggetto Cd e poi chiama
Ongaku::Ui.aggiungi_cd:
def aggiungi
puts SEPARATORE
puts "Aggiungi CD"
Per rimuovere un cd visualizziamo la lista completa e poi chiediamo all'utente di digitare l'indice
del cd da eliminare. Tale valore è passato poi a Ongaku::Ui.elimina_cd:
def elimina
elenca
print "Scegli il CD da eliminare: "
index = gets.chomp
Ongaku::Ui.elimina_cd(index.to_i - 1)
end
Dove elenca è il metodo utilizzato per visualizzare la lista completa, non facciamo altro che
stampare tutti gli elementi dell'array preceduti dal numero d'ordine:
def elenca
puts SEPARATORE
puts "Elenco dei Cd in archivio"
$archivio.each_index {|i| puts "#{(i + 1).to_s.rjust(4)}: #{$archivio[i]}"}
end
Infine quando si sceglie di chiudere l'applicazione con il comando q viene scritto l'array sul file con
il metodo Ongaku::Ui.salva_cd e viene stampato un messaggio d'uscita.
Ongaku::Ui.salva_cd
puts "Ora la lista contiene #{$archivio.size} CD"
puts "Grazie per aver utilizzato #{NOME}!"
exit
Per quel che riguarda le altre due interfacce, per ora, gli facciamo solo stampare un messaggio
d'errore qualora siano invocate.
L'applicazione in azione
A questo punto non resta che provare quello che abbiamo fatto. Cominciamo dalle opzioni:
$ ruby ongaku.rb -h
Ongaku è un semplice catalogatore di CD
ongaku.rb -i [text|gui|web]
$ ruby ongaku.rb -i foo
Interfaccia non implementata
Utili esercizi
Anche l'occhio inesperto si sarà accorto che Ongaku è ampiamente migliorabile sotto molti aspetti.
Lascio dunque al lettore come utile esercizio l'implementazione delle funzionalità mancanti, ecco di
seguito un elenco incompleto:
• Vanno aggiunti i commenti al codice
• Va aggiunta una funzione "Modifica CD"
• Sarebbe utile poter esportare la lista di cd in diversi formati
• Va aggiunta la gestione delle eccezioni
• Sarebbe utile anche la creazione di un file di configurazione
• Si può migliorare ampiamente la resa grafica
Altri aspetti invece riguardano argomenti che tratteremo nella seconda parte di questa guida come
ad esempio l'implementazione delle interfacce Gui e Web, la creazione di test unitari,
l'internazionalizzazione e così via.
Note finali
Come già detto si è cercato con questo esempio di fornire un supporto didattico alla guida, quindi
molte soluzioni non sono delle migliori ma sono state adottate perché bene si adattano allo scopo
prefisso. Inoltre è utile ricordare che è buona norma, soprattutto in previsione di una distribuzione,
dare dei nomi inglesi ai moduli alle classi e ai metodi; l'italiano è usato nell'esempio per non
aggiungere ulteriori difficoltà ai neofiti.