Io ricordo ancora, a distanza di anni, il mio primo programma, era
10 print "pippo"
20 goto 10
E girava su un c64 nel lontano 1988. Ricordo l'emozione provata: quel senso di creazione, di capire il funzionamento di qualcosa, tutta quella potenza in due sole righe! Cercate di capirmi, avevo otto anni, ed era la prima volta che provavo il piacere della programmazione.
Nella vita reale però ci si trova ad usare strumenti reali, non giocattoli, ed alla lunga, perdiamo questo piacere. Siamo spesso li a sbattere la testa su una
free()
o una
strcat()
mal gestite che mandano all'aria un sistema apparentemente perfetto, o imprechiamo per dover scrivere
System.out.println(myObj.toString());
piuttosto che il
print che avremmo usato a 8 anni.
Se vi riconoscete, almeno minimamente in questa frustrazione, forse ruby è il linguaggio che fa per voi.
Ruby nasce in Giappone grazie a Yukihiro Matzumoto (Matz, per tutti) nel 1994 (la release 1.0 è del 1996) basandosi su pochi concetti semplici ma nel loro piccolo "
rivoluzionari", che illustreremo tra poco.
L'essere sviluppato originariamente nel paese del Sol Levante ha ridotto la visibilità del linguaggio per qualche tempo, poiché la base di programmatori originaria tende a scrivere nella propria lingua, poco nota altrove.
Ovviamente avere una base di sviluppatori che scriva "nativamente" in una lingua diffusa come l'inglese ha permesso ad altri linguaggi di diffondersi in minor tempo, mentre ruby ha impiegato parecchio a valicare i proprio confini originari (all'interno dei quali è diffusissimo, basti pensare che in Giappone sono stati pubblicati l'anno scorso quasi trenta libri su ruby).
Ormai però la comunità di sviluppatori e la letteratura di lingua inglese ha raggiunto la massa critica, e di conseguenza la visibilità (e lo sviluppo del linguaggio) sono in crescita velocissima.
È interessante notare che Il progetto originario è stato variato pochissimo nel tempo (sostanzialmente con la sola introduzione di variabili di classe, poetry mode per le chiamate a metodi, e statement modifier in stile perl), indice che il design iniziale era comunque molto valido.
Ma torniamo alle "
spinte ideologiche" dietro il linguaggio. Matz nel progettare ruby ha puntato su pochi concetti fondamentali:
- un linguaggio dovrebbe far concentrare sul risolvere il problema non su come spiegare al compilatore il modo di risolvere il problema
- un linguaggio dovrebbe aiutare il programmatore a non fare errori
- un linguaggio dovrebbe permettere di automatizzare qualsiasi compito banale
Matz si auto-definisce un "language maniac", la sua passione per il design di vari linguaggi di programmazione ha generato ruby come una
summa di approcci considerati utili o interessanti provenienti da sistemi anche molto diversi tra loro.
Alcune delle caratteristiche di Ruby
Interpretato
Ciò permette di evitare tempi morti per la compilazione, e problemi di portabilità del codice.
Ovviamente il lato negativo di questo approccio è una minore efficienza del codice.
Per ovviare a questo, Ruby fornisce un sistema di interfacciamento con C/C++ molto semplice ed efficiente,
dunque è possibile usare librerie C esistenti, o riscrivere le parti meno efficenti delle proprie applicazioni in maniera che raggiungano prestazioni migliori.
Inoltre, a detta di molti l'interfaccia verso il C di Ruby è una delle più potenti ed intuitive, rendendo la programmazione di moduli in C simile a quella in Ruby stesso.
Completamente Object Oriented
Ormai da tempo si è imposto l'uso della progettazione ad oggetti per ottenere codice facilmente testabile, manutenibile e di miglior comprensione. Ruby ha un approccio alla OOP ispirato a Smalltalk, e più puro rispetto a Python, C++ e persino rispetto a Java. Non esistono tipi base che non siano oggetti in ruby (pensate a
int/
Integer in Java), e persino un normale numero inserito da tastiera viene considerato un oggetto e possiede i propri metodi:
5.times do
print "Mi piacciono gli oggetti!"
end
Un approccio di questo tipo permette una estrema coerenza e prevedibilità del linguaggio, togliendo al programmatore il peso di dover sapere di volta in volta
con cosa stia lavorando, e dunque come debba farlo.
Gestione strutturata delle eccezioni
I linguaggi moderni (Java, Python, C# ...) fanno tutti uso di variazioni sul tema di
prova()..solleva(miaEccezione)..recupera(TipoEccezione) poiché ciò permette di isolare la normale logica di controllo da quella dedicata al recupero da situazioni impreviste.
In linguaggi che ne sono sprovvisti, come il C , ci si trova costretti ad un controllo continuo su ogni chiamata a funzione suscettibile di fallimento, intrecciando continuamente le due cose e rendendo meno leggibile il codice.
Ad esempio usando C su UNIX avremmo cose del genere:
/* normale codice */
if (chiamataSistema() == -1 ) {
if (errno!= EINTR) {
/* gestione errore 1 */
}
}
/* altro codice normale */
if (altrasyscall() == 0) {
/* gestione errore 2 */
}
/* resto del codice */
In ruby faremmo una cosa del genere:
codice e syscall
rescue SystemCallError
gestione eccezione
Sintassi minimale
Avere una sintassi semplice permette di apprendere il linguaggio in breve tempo, e allo stesso tempo permette a qualsiasi sviluppatore di comprendere codice scritto da altri.
L'idea è che non si debbano aggiungere regole sintattiche per tappare sempre nuovi buchi, ma eliminare le debolezze che rendono tali regole necessarie.
Una sintassi ristretta , ma ben progettata, permette al linguaggio di "
auto-espandersi" rimanendo sempre simile a se stessa, ad esempio sono stati implementati "in Ruby" le
assertion per le suite di Test e delle versioni basilari di
design by contract, pur non essendo previsti dall'inizio.
Esiste persino un progetto per usare Ruby come HDL.
Variabili non tipizzate, ma strong typed
Per evitare la confusione derivante dal weak typing ruby mantiene il tipo dei dati in memoria, pur considerando le variabili come dei semplici riferimenti ad oggetti, e quindi senza tipo.
Spesso chi proviene da linguaggi come Java o C++ pensa che questo approccio (comune a molti linguaggi interpretati) sia causa di continui problemi, problemi che in linguaggi con variabili tipizzate possono essere risolti in fase di compilazione, piuttosto che restare latenti.
In effetti però basta pensare all'uso in Java di una
HashMap o di
Vector per comprendere come questa ipotesi sia errata.
Buona parte del codice scritto è soltanto un continuo cast da e verso Object, che con linguaggi come python o ruby è del tutto inutile:
Miaclasse mia = (Miaclasse) mioVector.elementAt(10)
Inoltre, il compilatore non può garantirci che l'elemento restituito sia effettivamente di classe
Miaclasse, in quanto la collezione su basa su
Object. Dobbiamo quindi porre un controllo per assicurarci che l'oggetto restituito sia convertibile con un cast a Miaclasse.
In realtà, quello che stiamo facendo è cercare una soluzione ad un
problema generato dal fatto stesso di avere variabili con tipo.
In ruby sarebbe bastato fare
(gli array in ruby sono dinamici come i Vector in java)
La scelta di far esistere variabili parametriche, ad esempio nei template del C++ (e forse in java 1.5 ?) può essere vista come un modo per aggirare le limitazioni di variabili con tipizzazione statica.
Sul dualismo weak/strong typing può essere interessante leggere
[2] ed il thread che parte qui
[3].
È inoltre da notare l'esistenza di una libreria che permette di avere una certa forma di strong typing anche in ruby
[4].
Garbage Collection automatica
Sebbene molti ancora storcano il naso al riguardo, l'uso di una GC automatica non è meno efficiente di una gestione manuale della memoria, non a caso moltissimi linguaggi negli ultimi anni hanno fatto una scelta di questo tipo : VB, C#, Java, Perl, per dirne alcuni. Esistono anche esempi di grosse applicazioni che fanno uso di librerie per la GC automatica in C, ad esempio X11
[5].
Classi/funzionalità di base potenti ed integrate
Ad esempio l'integrazione di espressioni regolari, Hash/dizionari, Array dinamici, thread in userland, pattern comuni come Delegate, Observer, Visitor/Iterator, Singleton, Decorator, Introspection, ... integrati direttamente nel linguaggio o in moduli appositi della libreria standard.
POLS e syntax sugar
Probabilmente è il concetto fondamentale dietro Ruby è, quello relativo a
POLS e
syntax sugar.
POLS sta per
Principle Of Least Surprise ed indica la scelta di rendere l'intero sistema ruby coerente e prevedibile: tutto è un oggetto, i metodi hanno nomi ovvii ed auto-esplicativi e la maggior parte delle funzionalità che ci si aspetta che esistano ... beh, esistono :-)
Ad esempio, come ordinare un array? basta un
array.sort
Come fare a definire classi/metodi in maniera condizionale?
if condizione
definizione
end
Inoltre ci vengono fornite molte delle funzionalità a cui si è abituati in altri linguaggi; chi provenisse dal C++ potrebbe desiderare di usare l'operatore "
<<" per l'output.
Ruby fornisce dunque un metodo "
<<" per gli oggetti
IO implementato in maniera da modificare l'oggetto e restituirlo di nuovo. In questa maniera è possibile scrivere:
out << "ciao" << " a " <<"tutti!"
semplicemente facendo riferimento ad un metodo dell'oggetto IO, ma senza rendersene conto.
Il syntax sugar rappresenta invece la scelta da parte dei progettisti di permettere una scrittura naturale senza rovinare la natura OO del linguaggio. Un esempio è l'esistenza dei vari operatori, come "
+,-,*,<,==,<=>" etc.
In realtà si tratta di metodi definiti nelle rispettive classi, invocabili però senza la sintassi classica
obj.metodo.
L'eleganza di questo approccio è evidente se si pensa a come il metodo "
+" possa essere usato per lavorare su Integer, Float, String, Array ed altre classi, lasciando alla classe il compito di gestirne il comportamento.
Un esempio collegato è l'esistenza di una sintassi
per
Ruby si preoccupa di definire per noi il comportamento dell'assegnazione "
+=" (e "*=" , "%=" etc.) nel momento in cui definiamo l'operazione "
+"
Il concetto che si individua dietro questi approcci è pervasivo in ruby ed è la caratteristica che veramente lo distingue da altri linguaggi:
rendere semplice la vita del programmatore.
Su questa stessa logica si basa anche l'esistenza degli iteratori, dei blocchi/lambda, dei singleton method e dei mix-in, ma di questo parleremo nella seconda parte, se avrete la voglia di leggerci ancora.