Ruby

II parte: approfondiamo alcuni aspetti del linguaggio Rubydi

Approfondiamo alcuni aspetti del linguaggio Ruby ponendo l‘attenzione su alcuni argomenti piuttosto particolari. Tra i vari che passeremo in rassegna, faremo riferimento anche a Modules e Mixins, e al Duck Typing, una filosofia di programmazione utilizzabile in Ruby. Quest‘ultima ha fatto molto parlare di sé, nel bene e nel male, portando gli addetti ai lavori a qualche contrapposizione (ideologica!!!).

Introduzione

Nel presente articolo verranno descritti vari aspetti del linguaggio: alcuni sono più "teorici" e vanno tenuti presenti nella fase di progettazione software; altri sono molto più "pratici" e risultano utili al programmatore al lavoro. E il primo argomento pratico che affronteremo è proprio la disponibilità  di ambienti di sviluppo integrati (IDE).

Perchè questi argomenti piuttosto eterogenei? Diciamo che sono tutti essenziali per consentire un facile approdo a Ruby da parte di uno sviluppatore medio, in particolare proveniente da Java, e per renderlo operativo velocemente. Per intenderci, diciamo che non si può lavorare seriamente senza un IDE valido o senza conoscere le Collections o non avendo idea di cosa sia un Module o un Mixin.

Ruby IDE

Nell‘articolo del mese scorso abbiamo citato FreeRIDE.

Lo strumento consente di gestire progetti, di sfogliare la documentazione dei pacchetti base, ha integrata l‘interazione con irb e il debugger: anche se a me non ha mai funzionato e mandava in crash l‘IDE (la versione era la 0.9.6 per Linux), può essere considerato un punto di partenza, in particolare in ambiente Windows in cui è disponibile nell‘installer dei pacchetti base di Ruby, ma nulla di più. FreeRIDE risente della mancanza di una serie di caratteristiche che lo rendano efficace in un‘ambiente produttivo e cioè integrazione con strumenti di versioning, supporto per Rails, disponibilità  di plug-in e wizard vari che velocizzerebbero notevolmente l‘attività  lavorativa.

Figura 1 - FreeRIDE al lavoro

Possiamo considerarlo un editor adatto per scopi didattici.

Indubbiamente più professionale è RadRails. Figlio di Eclipse, quindi con un‘ impostazione familiare a molti programmatori Java, consente una gestione dei progetti in modalità  sicuramente superiore rispetto al suo concorrente. Pur se ancora imperfetto sotto vari aspetti, sta crescendo molto rapidamente.

Figura 2 - RadRails

Nel caso in cui avessimo sulla nostra macchina una installazione di Eclipse (ma, anche nel caso in cui Eclipse non sia presente, nessuno ci vieta di farlo...) possiamo semplicemente aggiungere il Ruby Development Tools plug-in (http://rubyeclipse.sourceforge.net/) e avere in un‘unico ambiente il supporto per Java e per Ruby. L‘aggiornamento può essere effettuato anche utilizzando l‘Update Manager di Eclipse.

I tre tool appena visti sono i principali IDE gratuiti da prendere in considerazione per semplificarsi la vita in Ruby.

Voltiamo pagina e passiamo a trattare le Collections. Le Collections non sono certo una novità  di Ruby, ma il loro utilizzo è assolutamante da conoscere per le enormi potenzialità  a disposizione.

Collections (o Containers)

Innanzitutto definiamo che cosa è una Collection. Diciamo che si tratta di un oggetto utilizzato per contenere e gestire un insieme di altri oggetti (correlati tra di loro). A seconda del linguaggio di programmazione che stiamo utilizzando le possiamo trovare definite anche come Containers.

Descriviamo sinteticamente le tre principali strutture associabili al concetto di Collection in Ruby.

Range

Il più intuitivo tra le Collection. Si tratta di un contenitore dei singoli elementi di un intervallo. L‘utilizzo più naturale del Range è quello di una sequenza di valori numerici. Apriamo una shell e andiamo su irb. Digitiamo

primorange = 30...48

Questo significa disporre di un Range con tutti gli elementi tra 30 e 48 estremi esclusi. Se invece di tre punti, avessimo digitato .. (due punti), sarebbe stato estremi inclusi.

Ã? possibile avere anche un Range con tutte le lettere tra a e j.

secondorange = ‘a‘..‘j‘

Testiamo la presenza del numero 35 su primorange

primorange.include?(35)

Otteniamo

=> true

Iteriamo sull‘altro range e mandiamo a video il risultato

secondorange.each do |e|
print e, ‘ ‘
end

e avremo come risultato

a b c d e f g h i j => "a".."j"

Molto comodo ed efficace!

Array

Passiamo a un più usuale array. In Ruby è presente la classe Array che contiene una collezione di "object reference". Ogni reference ha una propria posizione nell‘array. La posizione viene identificata da un intero non negativo (a partire dalla posizione 0). Fin qui nulla di particolare. Passiamo alla codifica.

Creiamo un array vuoto

mioarray = []

oppure

mioarray = Array.new

Passiamo ad un array con contenuto non vuoto.

a = ["a", 1, "eccomi"]

Probabilmente sembrerà  strano avere un array di questo tipo (che gioco di parole!) ma in Ruby è possibile avere un Array che contiene elementi che non sono dello stesso tipo. Questa è la prima caratteristica abbastanza singolare. Altra caratteristica è che non è necessario specificare il tipo di array. Quindi si tratta di un‘istanza di Array e contiene qualsiasi cosa (stringhe, numeri, valori restituiti da metodi, etc.).

Facciamo qualche operazione. Prendiamo i dati dall‘array e mandiamoli in output

puts(a[1])

Settiamo un valore al posto 1 dell‘array

a[1]=34

Introduciamo il valore nil alla posizione 2.

a[2]=nil

Avremo quindi il seguente array

a = [34, nil, "eccomi"]

Velocizziamo la creazione dell‘array di stringhe utilizzando

a = w%{ pippo, pluto, paperino, clarabella}

invece di

a = [ ‘pippo‘, ‘pluto‘, ‘paperino‘, ‘clarabella‘]

Altra caratteristica della classe Array in Ruby risulta essere il numero molto elevato di metodi che consente di utilizzarlo in molti contesti e in modo estremamente flessibile. Per esempio come una coda, uno stack, etc... cosa che in altri linuguaggi è possibile solo con una classe maggiormente "specifica" rispetto al generico array. Date un‘occhiata alla documentazione per verificare le operazioni effettuabili.

Riassumendo, diremo che un array in Ruby non ha tipo, contiene oggetti di tipo diverso ed è estremamente flessibile grazie al numero di operazioni già  definite. Questo implica che è utilizzatissimo.

Hashes (o Maps, Dictionary)

Si tratta di un cugino stretto dell‘array. La differenza fondamentale rispetto all‘array consiste nel fatto che è possibile associare testo (o qualsiasi oggetto) come "chiave". Questo lo rende non ordinabile. Ecco un esempio:

myhash = { ‘Sud‘ => ‘Sole‘, ‘Centro‘ => ‘Ministeri‘,‘Nord‘ => ‘Lavoro‘, ‘Isole‘ => ‘Mare‘ }

Nulla di nuovo sotto il sole in questo caso.

Voltiamo di nuovo pagina e parliamo di Module e Mixins.

Modules e Mixins

In generale al crescere della dimensione delle applicazioni sono sempre più necessari pezzi di codice riusabile in più punti del software, in special modo metodi. Al fine di evitare la loro riscrittura, una possibile banale soluzione potrebbe essere quella (approccio C-like) di far riferimento ai file in cui sono contenuti i metodi in questione. Altra soluzione (in Ruby) è quella di utilizzare i Modules. Facciamo un esempio di Module.

module Quadrato
NUMEROLATI=4
def Quadrato::area(x)
x=x*x;
end
def Quadrato::perimetro(x)
x=NUMEROLATI*x;
end
end

Abbiamo definito dei metodi e introdotto una costante nel modulo (come se fosse una classe) a cui si può far riferimento dalle classi che importano il modulo. Supponiamo di salvare nel file quadrato.rb il codice sopra riportato. Usiamo il modulo da un altro programma.


require ‘quadrato‘
@lato=5
@area = Quadrato::area(@lato)
@perimetro = Quadrato::perimetro(@lato)
puts("Area #@area")
puts("Perimetro #@perimetro")
puts(Quadrato::NUMEROLATI)

La struttura dei Module è qualcosa di più che un‘alternativa al "mettere codice in comune". Per far fronte alle nostre esigenze progettuali, in Java ricorriamo all‘utilizzo di classi (eventualmente astratte) e a interfacce; il linguaggio Ruby, invece, mette a disposizione classi, modules e un insieme dei due (mixins).

Notiamo come il Module sia una struttura abbastanza lontana dal mondo Java. Non può essere istanziata, analogamente a quanto avviene in Java per una classe astratta o un‘interfaccia; ma a differenza di queste, il Module Ruby è assolutamente implementabile. Inoltre aggiungiamo che non è possibile estenderlo in alcun modo, cioè non è possibile fare l‘analogo dell‘"extends" o dell‘"implements" Java.

Il motivo principale per il quale si fa ricorso alla struttura del Module è comunque legato al fatto che i metodi del Module possono essere condivisi tra varie classi. Spieghiamo meglio questo concetto introducendo la struttura dei mixins e introducendo la keyword include.

Innanzitutto precisiamo che si tratta semplicemente di un "reference" e non di una inclusione vera e propria. L‘utilità  dell‘operazione consiste nel fatto che in un unico file possiamo definire più classi e tutte possono far riferimento allo stesso "Module". Facciamo un esempio.

module Operazioni 
def somma (addendo1, addendo2)
return addendo1+addendo2
end
def differenza (addendo1, addendo2)
addendo1-addendo2
end
def prodotto (a,moltiplicatore)
return a*moltiplicatore
end
def divisione (dividendo, divisore)
return dividendo/divisore
end
end
class CalcolatriceScientifica
include Operazioni
def initialize
#.....
end
#.....
end

Cosଠfacendo abbiamo definito dei metodi nel Module Operazioni, poi lo abbiamo "incluso" nella classe Calcolatrice. In tal modo, i metodi sono stati tutti ereditati e sono quindi utilizzabili all‘interno della classe.

c = CalcolatriceScientifica::new
risultato=c::somma(1,1)
puts(risultato)
...

Come si può facilmente intuire i mixins sono uno strumento molto potente. Proviamo a simulare una sorta di ereditarietà  multipla. Supponiamo che la classe CalcolatriceScientifica estenda la classe Calcolatrice e ne erediti quindi attributi e metodi; aggiungendo anche l‘include di un Module, avremmo disponibili anche tutti i metodi lଠdefiniti.

class CalcolatriceScientifica < Calcolatrice
include Operazioni
#....
end

Introduciamo a questo punto un utilizzo molto interessante (e divertente) dei Module.

Supponiamo di avere a disposizione il seguente:

module OperazioniScientifiche
def radicequadrata (number)
Math.sqrt(number)
end
#...
end

A questo punto supponiamo di avere una necessità  "occasionale" dei metodi definiti in OperazioniScientifiche. Utilizzando direttamente una istanza di CalcolatriceScientifica (o di un‘altra qualsiasi classe), possiamo fare quanto di seguito mostrato:

c.extend OperazioniScientifiche
r = c.radicequadrata(4)

Chiamando extend sull‘istanza c, viene estesa niente altro che c. In tutte le altre istanze di CalcolatriceScientifica non sarà  disponibile il metodo radicequadrata.

La struttura dei mixins diventa assolutamente utile nel momento in cui iniziamo ad utilizzarla per aggiungere funzionalità  alle nostre classi includendo quelle di alcune classi standard. In tal senso gli esempi più interessanti sono Comparable ed Enumerable (Vedi RDoc dei pacchetti base). Si fa riferimento a Comparable nel caso in cui sia necessario effettuare un confronto tra due oggetti. Includendo il riferimento è possibile utilizzare tutti i metodi di confronto disponibili.

Figura 3 - RDoc della classe Comparable

Con il medesimo meccanismo, Enumerable, utilizzato tra l‘altro anche dalla classe Array, consente di rendere "numerabile" (diciamo simile ad una Collection ordinata) qualsiasi classe. Ã? sufficiente che

class NostraClasse 
include Enumerable
...
end

e l‘istanza di NostraClasse disporrà  di tutti i metodi di Enumerable. Semplicissimo ed estremamante potente! Concludiamo la nostra carrellata di argomenti con qualche riferimento al Duck Typing.

Classe, tipo e duck typing

Descriviamo brevemente il Duck Typing e passiamo poi a vederne le implicazioni che seguono al suo utilizzo in Ruby.

"In informatica, duck typing è un termine che indica un tipo dinamico, tipico di alcuni linguaggi di programmazione come Small Talk, Python, Ruby e ColdFusion, dove è il valore stesso della variabile a determinare ciò che la variabile può fare. Pertanto, un oggetto avente tutti i metodi descritti in un‘interfaccia può essere condotto a implementare l‘interfaccia dinamicamente a runtime, anche se la classe dell‘oggetto non include tale interfaccia nella sua clausola ‘implements‘. Ciò comporta inoltre che un oggetto diventa intercambiabile con ogni altro oggetto che implementi la stessa interfaccia, indipendentemente dal fatto che l‘oggetto possegga una gerarchia di ereditarietà  collegata". (traduzione da Wikipedia, The Free Encyclopedia).

Teniamo presente la definizione sopra riportata e facciamo qualche approfondimento. Risultano d‘obbligo alcune precisazioni. Con "interfaccia" ci riferiamo non all‘ Interface Java quanto piuttosto all‘insieme dei metodi cui un oggetto risponde. Inoltre per "tipo" intendiamo il nome utilizzato per denotare una particolare interfaccia.

Classe e tipo

A questo punto evidenziamo la differenza tra classe e tipo e per far questo ricorriamo a una citazione presente in "Design Patterns" della Gang of Four:

"Ã? importante capire la differenza fra la classe di un oggetto e il suo tipo. La classe definisce come è stato implementato. Definisce il suo stato interno e l‘implementazione delle sue operazioni. D‘altra parte il tipo di un oggetto si riferisce solo alla sua interfaccia, all‘insieme di richieste a cui può rispondere. Un oggetto può avere molti tipi, e oggetti di classi differenti possono avere lo stesso tipo".

Naturalmente c‘è una una stretta parentela fra classe e tipo. Quando una classe definisce le operazioni che un oggetto è in grado di eseguire, definisce anche il suo tipo. Quando diciamo che un oggetto è un‘istanza di una classe, diciamo implicitamente che l‘oggetto supporta l‘interfaccia definita dalla classe.

Ereditarietà  di classe e di tipo

Fatte queste precisazioni, distinguiamo fra ereditarietà  di classe e ereditarietà  di tipo. L‘ereditarietà  di classe si basa sul definire l‘implementazione di un oggetto attraverso l‘implementazione di un altro oggetto. L‘ereditarietà  di tipo è invece relativa all‘utilizzare un oggetto al posto di un altro.

Java vs Ruby

Dopo aver messo in gioco questi concetti, cerchiamo di fare un pò di ordine. Ipotizziamo di essere abituati a programmare in Java, o in un qualche altro linguaggio "tipato" e di intendere una Classe al pari di un Tipo (Questo concetto in realtà  non corrisponde al vero nemmeno in un linguaggio come Java... basti pensare alle interfacce e alle classi che ne implementano magari più di una).

Abbiamo in mente:

  • un oggetto è un‘istanza di una classe
  • la classe è il nostro tipo di riferimento

In Ruby invece la classe non è mai (o quasi mai) il tipo. Arrivando a Ruby da linguaggi statically-typed, molti intravedono una sorta di incertezza o meglio pericolosità  dovuta alla alta interscambiabilità . In ogni caso si può scegliere di scrivere codice anche in modalità  "più tradizionale" o comunque senza estremizzare il concetto.

Riassumiamo i concetti fin qui espressi dicendo "gli oggetti in Ruby sono fortemente tipati; i metodi e le variabili invece non lo sono. La classe non rappresenta il tipo ma è piuttosto il suo comportamento a definirlo."

Se si vuole programmare usando la filosofia tipica del duck typing è importante ricordare che il tipo di un oggetto è determinato dal suo comportamento e non dalla sua classe.

"Se un ‘oggetto‘ si muove come una papera e fa ‘qua qua‘ come una papera, chi deve interpretare la sua natura lo considererà  proprio come una papera" (Dave Thomas). Ã? da questa considerazione, basata su un modo di dire americano, che nasce appunto il termine "duck" typing.

Ã? altresଠfondamentale specificare che si tratta solo di uno stile di programmazione non di un insieme di regole da rispettare. A riguardo di questo argomento si trovano spesso (in forum, blog, etc.) controversie tra sostenitori (spesso estremi) delle due impostazioni. Provate ed esplorate il duck typing. A voi la scelta riguardo al punto fin dove è bene spingersi e cosa utilizzare.

Facendo un esempio di duck typing, mostriamo un‘altra potenzialità  di Ruby, quella legata alla facililtà  di implementazione di test di unità . (Ruby dispone nel pacchetto base di un framework con tutto il necessario per realizzare velocemente classi di Unit Testing). A tal fine utilizziamo un esempio tratto da "Programming Ruby" di Dave Thomas.

Disponiamo di una classe Customer con un metodo append_name_to_file per aggiungere il nome e cognome nel file con il quale stiamo lavorando.

class Customer
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def append_name_to_file(file)
file << @first_name << " " << @last_name
end
end

Scriviamo il nostro unit test. Utilizzando un approccio "tradizionale" avremmo scritto qualcosa del tipo:

require ‘test/unit‘
require ‘customer‘ # il precedente codice è salvato in un file con questo nome

class TestAddCustomer < Test::Unit::TestCase
def test_add
c = Customer.new("Ima", "Customer")
f = File.open("tmpfile", "w") do |f|
c.append_name_to_file(f)
end
f = File.open("tmpfile") do |f|
assert_equal("Ima Customer", f.gets)
ensure
File.delete("tmpfile") if File.exist?("tmpfile")
end
end

Che cosa abbiamo fatto? Abbiamo istanziato la classe, aperto il file, fatto l‘append al file del testo, verificato che il contenuto del file corrispondesse ai "desiderata", quindi eliminato il file (solo se esiste).

Con un approccio più orientato al duck typing avremmo scritto:

require ‘test/unit‘
require ‘customer‘
class TestAddCustomer < Test::Unit::TestCase
def test_add
c = Customer.new("Ima", "Customer")
f = ""
c.append_name_to_file(f)
assert_equal("Ima Customer", f)
end
end

Non abbiamo scritto in alcun file, ma abbiamo usato una stringa e poi abbiamo effettuato la verifica su questa. Meditate gente meditate...

Conclusioni

Ora che abbiamo un‘idea di cosa è Ruby, di dove si pone e di quali sono le sue caratteristiche principali (e le differenze principali da Java) siamo pronti per iniziare a trattare, dal prossimo articolo, uno dei motivi che ne sta decretando il successo a livello internazionale: il framework Rails. Ovviamente la nostra trattazione è stata tutt‘altro che esaustiva, ma ci auguriamo che gli input presenti, cosଠcome i riferimenti citati, funzionino da stimolo ai necessari approfondimenti che l‘argomento merita. Intanto potete verificare gli esempi citati scaricando l‘allegato nel menu in alto a sinistra.

Riferimenti

[1] Dave Thomas, "Programming Ruby. The Pragmatic Programmer‘s Guide", Pragmatic Bookshelf, 2004

http://www.pragmaticprogrammer.com

[2] Dave Thomas, "Agile Software Development with Rails. The Pragmatic Programmer‘s Guide", Pragmatic Bookshelf, 2005

http://www.pragmaticprogrammer.com

[3] Wikipedia

http://www.wikipedia.org/

[4] FreeRIDE

http://freeride.rubyforge.org

[5] RadRails

http://www.radrails.org

[6] Ruby Development Tools

http://rubyeclipse.sourceforge.net/

[7] Why‘s poignant guide to ruby

http://poignantguide.net/ruby/

[8] Juixe TechKnow

http://www.juixe.com/techknow/index.php/2006/06/15/mixins-in-ruby/

[9] Mr. Neighborly‘s Humble Little Ruby Book

http://www.infoq.com/minibooks/ruby

Condividi

Pubblicato nel numero
115 febbraio 2007
Laureato in Scienze Statistiche e particolarmente attento alle novità che lo circondano, dal 1998 lavora nel settore dell‘IT. Prevalentemente coinvolto in progetti legati a Java, da un paio d‘anni è interessato al mondo Open Source. Dopo aver accumulato una notevole esperienza presso vari clienti e con vari ruoli (da programmatore…
Articoli nella stessa serie
  • Ruby
    I parte: introduzione al linguaggio
  • Ruby
    III parte: Iniziamo a percorrere i binari di Rails
  • Ruby
    IV parte: Rails... non solo Web Services
  • Ruby
    V parte: Ruby on Rails e Web 2.0
Ti potrebbe interessare anche