Articoli Manifesto Tools Links Canali Libri Contatti ?
Linguaggi / Ruby

Ruby, un'introduzione (ovvero: Programming should be Fun) (seconda parte)

Abstract
Dopo aver introdotto la filosofia di Ruby, in questo articolo affrontiamo nuove caratteristiche del linguaggio come classi, oggetti e lambda e la sua enorme dinamicità.
Data di stesura: 04/06/2003
Data di pubblicazione: 28/06/2003
Ultima modifica: 04/04/2006
di Gabriele Renzi Discuti sul forum   Stampa

In collaborazione con Gruppo Utenti Ruby Italia

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:

  1. 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:

  1. iniziale=10 
  2. def  contatore( n ) 
  3.             proc do 
  4.              val = n 
  5.              n = n + 1 
  6.              val # valore restituito 
  7.             end 
  8. end 
  9.  
  10. c1=contatore(iniziale) 
  11. p c1.call #10 
  12. p c1.call #11 
  13. p c1.call #12 
  14. iniziale=30 # non influisce su contatore, perchè questo si è  
  15.             # "chiuso" su  iniziale=10 
  16. c2=contatore(iniziale) 
  17. p c2.call #30 
  18. p c1.call #13 ! non veniamo influenzati 
  19. 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:

  1. File.open ("miofile.txt") { |fd|  
  2.   fd.readlines.each_with_index { |linea, num|  
  3.     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:

  1. def metodo(arg1,...,&blocco)  
  2.   yield param1,param2,param3,... 
  3. 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:

  1. def saluto (chi) 
  2.  return "Salve "+chi+"!" 
  3. end 
  4.  
  5. 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):

  1. class Object 
  2.    def to_xml 
  3.      print "< ",self.class," id=", self.id ,">\n" 
  4.         puts self 
  5.         instance_variables.each do |k| 
  6.          eval(k).to_xml 
  7.        end 
  8.     print "</",self.class,">\n" 
  9.   end 
  10. end 
e uno spezzone di prova:
  1. class Mia 
  2.   def initialize(x,y) 
  3.   @x=x 
  4.   @y=y 
  5.  end 
  6. end 
  7.  
  8. class Tua <Mia 
  9. end 
  10.  
  11.  
  12. a=Mia.new "ciaO",5 ; b=Tua.new a , 30.0  
  13. 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

  1. def with(object, &block) 
  2.  object.instance_eval(&block) 
  3. end 
  4.  
  5. with "1" do 
  6.  puts "La mia lunghezza e` #{length}" 
  7. 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:

  1. class LoggerObj  
  2.   def initialize classe, *args 
  3.     @obj = classe.new *args 
  4.   end 
  5.   def method_missing (method, *args, &block) 
  6.     @obj.send (method, *args, &block) 
  7.     log_method (method) 
  8.   end 
  9.   def log_method(met) 
  10.     print "chiamato metodo : " , met  
  11.   end 
  12. 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.

Informazioni sull'autore

Gabriele Renzi, studente di Ingegneria Informatica, è un appassionato di programmazione e sistemi operativi. Scrive "hello world" in una dozzina di linguaggi ma non riesce ad andare oltre nemmeno in uno.
Collabora con il Progetto Documentazione Italiana FreeBSD ed è membro del Gruppo Utenti Ruby Italia.

È possibile consultare l'elenco degli articoli scritti da Gabriele Renzi.

Altri articoli sul tema Linguaggi / Ruby.

Risorse

  1. Parte 1: Ruby, un'introduzione (ovvero: Programming should be Fun)
    http://www.siforge.org/articles/2003/05/26-ruby-intro-1.html
  2. Il linguaggio CLU.
    http://www.pmg.lcs.mit.edu/CLU.html
  3. La pagina del Ward's Wiki sulle chiusure ed il Lexical Scope.
    http://c2.com/cgi/wiki?ScopeAndClosures
  4. Le librerie NArray e Quanty, dedicate al calcolo scientifico.
    http://www.ir.isas.ac.jp/~masa/ruby/index-e.html
  5. Kconfig e Ruby.
    http://www.ussg.iu.edu/hypermail/linux/kernel/0210.2/1498.html
  6. Gioco scritto in C++/OpenGL che usa ruby come motore di scripting
    http://www.aie.act.edu.au/hail/
  7. L'Agile Manifesto.
    http://agilemanifesto.org/
  8. Ruby User Group Italia.
    http://ada2.unipv.it/ruby/
  9. Ruby Central.
    http://www.rubycentral.org
  10. Programming Ruby, ottimo libro disponibile online.
    http://www.rubycentral.com/book/
  11. Ruby Garden.
    http://www.rubygarden.org/
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.