Ruby language

Metodi                

Ed eccoci quindi arrivati ad uno degli strumenti cardine di questo e di molti altri linguaggi, i metodi. Un metodo, come è noto, è una sorta di sottoprogramma, quindi una sequenza di una o più istruzioni raggruppate, abbinata in maniera esclusiva ad uno o più oggetti. Proprio questo abbinamento è ciò che differenzia i metodi dalle funzioni, costituzionalmente molto simili ma indipendenti. Ruby è un linguaggio a oggetti al 100% per cui non ha senso parlare di funzioni, in realtà. Anche gli esempi che faremo in questo paragrafo pur non richiamando direttamente oggetti, di cui non abbiamo parlato saranno sempre e comunque metodi, anche se appariranno come funzioni globali, che Ruby abbinerà alla classe Object, che poi vedremo cos'è. Un metodo può ricordare anche un blocco ma diversamente da questo ha un nome ed inoltre non è richiamabile solo tramite un iteratore come invece accade, secondo quanto abbiamo visto, quando si ha a che fare con i blocchi. I metodi non sono evidentemente manipolabili in maniera diretta alla stregua oggetti ma è possibile creare un oggetto Method che rappresenta un metodo.
Si tratta certamente di elementi complessi e potenti in Ruby e nella prossima sezione parleremo anche degli aspetti relativi a chiusure e calcolo lambda che in qualche modo sono correlate ai concetti qui espressi. In conclusione di questa premessa ritengo che, insieme alle classi, questa parte riguardi quanto di più importante ci sia in questo linguaggio. Proprio la profonda ed evidente sinergia con le classi sarà in un certo qual modo la causa di ulteriori approfondimenti sui metodi che vedremo a suo tempo.

La keyword che introduce un metodo è def. La incontrerete spessissimo nel corso della vostra vita insieme a questo linguaggio. Formalmente si ha:

def nomemetodo (parametri opzionali)
corpo del metodo
end

Di seguito un esempio che soddisfa la definizione formale:

  Esempio 8.1
1
2
3
4
5
def saluto (nome)
print("ciao ", nome)
end

saluto("mondo")

Dalla riga 1 alla 3 abbiamo il nostro metodo che viene richiamato alla 5 passandogli il parametro richiesto dalla firma o signature del metodo stesso (in pratica ciò che si vede alla riga 1). In situazioni normali (vedremo tra breve le eccezioni) se un metodo vuole un parametro è obbligatorio fornirglielo così come se ne vuole due ecc...mentre se non ne vuole è sbagliato cercare di passarglielo. In base a ciò cambiare la riga 5 come segue:

saluto
saluto()

in entrambi i casi dà origine ad errore. Come detto Ruby è un linguaggio a tipizzazione dinamica per cui potreste anche indicare un parametro numerico come argomento alla riga 5 ed il programma funzionerebbe lo stesso stampando a video il numero

Il nome del metodo deve iniziare con una lettere minuscola o con il segno di _ (underscore) seguiti poi da lettere, numeri o ulteriori undescore. Il segni di underscore è anche usato per separare più parole compiute che compongano il nome di un metodo, quindi scrivere questo_metodo è meglio, più chiaro se volete,  rispetto a scrivere questometodo. La cosa interessante e forse un po' ambigua (a me non piace tanto questa cosa, devo essere sincero) è che l'interprete non vi da errore se date ad un metodo un nome che inizia con la maiuscola. I problemi possono venire in fase di richiamo di quel metodo potendo lo stesso essere interpretato come una costante e quindi, in casi particolari, generare un errore. Per convenzione, come specificato da più parti, solo i metodi di conversione di tipo iniziano con la maiuscola. Visto che tutto sommato adeguarsi alle convenzioni è solo una questione di abitudini consiglio caldamente di tenere presente quanto qui indicato.
Le nozioni intorno al nome di un metodo non finiscono qui. Avrete gia notato come esistono metodi predefiniti il cui nome termine col ! (punto esclamativo). Si tratta dei cosiddetti bang methods che sono considerati "pericolosi" in quanto hanno effetti potenzialmente distruttivi anche sul chiamante, se guardate i paragrafi indietro ne abbiamo indicati un bel po'. Si tratta di una convenzione diffusissima nella libreria standard e nel mondo Ruby in generale, per cui tanto vale prenderla come regola.
Un altro terminatore diffuso relativamente al nome di un metodo è il ? (punto interrogativo). Tale suffisso è applicato ai metodi che restituiscono un valore booleano. Ne abbiamo visti a proposito di array ed hash, per esempio i metodi che riportanoo l'esistenza o meno di un elemento..
Infine è ammesso anche il segno di = che si applica in fondo al nome di metodi che possono stare a sinistra di un assegnamento. In questo caso il metodo è un "setter" nel senso che permette di settare, ovvero definire, il valore di una qualche altra entità.
Ripeto che si tratta di convenzioni (se un metodo si limita a stampare una stringa anche se il suo nome termina col punto esclamativo non può far nulla di male, siamo sempre noi a stabilire chi fa che cosa...) ma è anche importante, per uniformità, abbracciare questo modus operandi se davvero vi interessa Ruby.
Ancora, sempre relativamente alla nomenclature dei metodi, dobbiamo introdurre il meccanismo di aliasing. Come avrete capito si tratta di un meccanismo che permette ad un metodo di essere riconosciuto con più nomi diversi e si basa sulla keyword alias con il seguente formato:

alias nuovo_nome vecchio_nome

L'autore del linguaggio indica questo sistema come una via per rendere il linguaggio più espressivo e naturale e d'altronde lo abbiamo già incontrato nella libreria standard, come avrete notato esistono diversi metodi che fanno esattamente la stessa cosa su hash, array e così via, bene, questi ovviamente si basano su alias. E' importante specificare, come detto da più parti, che l'aliasing non è l'overloading. Quest'ultimo è un meccanismo che permette a due metodi di avere lo stesso nome ma comportamenti diversi, il tutto basato su una firma diversa tra metodi con lo stesso nome. In Ruby questo non si può fare. Tuttavia in questo linguaggio l'overloading non è necessario data la grande elasticità dei metodi come vedremo di seguito. Inoltre attraverso l'alias è possibile memorizzare un metodo anche se quello orginale dovrà essere modificato. Lo vediamo con un esempio:

  Esempio 8.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def saluto
puts("ciao")
end

alias saluto_2 saluto

saluto
saluto_2

def saluto
puts("saluto modificato")
saluto_2
end

saluto

 Dalla riga 10 viene riscritto il metodo "saluto" ma la sua copia "saluto_2" non viene intaccata da questa modifica, cosa facilmente verificabile eseguendo questo semplice programma. 

Ritornando all'esempio 8.1 riconsideriamo il discorso relativo ai parametri: come abbiamo accennato essi possono essere a piacere dal punto di vista numerico:

  Esempio 8.3
1
2
3
4
5
def somma (x,y,z,t)
print(x + y + z + t)
end

somma(1,2,3,4)

Ruby permette di attribuire ai parametri un valore di default; si parla di defaulted parameters. in questo caso è possibile, contrariamente a quanto visto per parametri non inizalizzati,  non fornire tutti i parametri richiesti in firma in quanto, in assenza di valori inviati dal chiamante verranno utilizzati quelli preassegnati; diversamente si utilizzeranno quelli che sono stati passati e che quindi sovrascrivono i valori di deafult. Il funzionamento è posizionale come spiega la tabella di esempio che segue

def somma (x = 1, y = 2, z = 3)
print (x + y + z)
end

Invocazione del metodo Esito
somma() o somma 6 ovvero 1 + 2 + 3
somma(10) 15 ovvero 10 + 2 + 3
somma(10, 20) 33 ovvero 10 + 20 + 3
somma(10, 20, 30) 60 ovvero 10 + 20 + 30

Credo la cosa sia abbastanza chiara: man mano che forniamo parametri i valori di questi vanno a sovrascrivere, da sinistra verso destra, quelli che sono predefiniti nella signature del metodo.
Una novità di Ruby 2.0 sono invece i cosiddetti named parameters o keyword arguments. La sintassi base di questi è la seguente:

nome: valore di default

bisogna tenere presente che questi parametri devono sempre essere messi dopo, ovvero a destra, rispetto ad eventuali parametri normali. Anche il passaggio di parametri verso il metodo segue la medesima sintassi.

  Esempio 8.4
1
2
3
4
5
6
7
def somma (x, y:1, t:6)
puts(x + y + t)
end

somma(1, t:8)
somma(3)
somma(1, t:9, y:19)

Con il seguente output:

10
10
29

la prima riga dell'output è data dalla somma 1 + 1 + 8, la seconda da 3 + 1 + 6, la terza da 1 + 19 + 9. Come si vede alla riga 7 l'ordine dei parametri nella firma del metodo non è stato mantenuto in fase di chiamata ma in questo caso va tutto a buon fine senza problemi dal momento che il nome attribuito al parametro rende univoca la sua identificazione.
Nelle vecchie versioni di Ruby si poteva simulare questo comportamento passando un hash come argomento; dato che è molto più comodo utilizzare i named parameters darò solo un rapido esempio:

  Esempio 8.5
1
2
3
4
5
6
7
def fn(a)
x1 = a[:x]
x2 = a[:y]
puts(x1 + x2)
end

fn({:x => 1, :y =>2})

Sempre relativamente al passaggio di prametri esiste anche la possibilità di trasmettere al metodo un numero variabile di argomenti. Ci sono d'altronde degli esempi comuni, come puts o print che gestiscono proprio un numero variabile di elementi. Per dar corso a questa possibilità bisogna ricorre al simbolo * (asterisco) che non è un vero e proprio operatore ma spesso nei testi è indicato come tale, posto davanti al nome di un parametro preferibilmente, ma non imperativamente, da mettere dopo i parametri normali, come si deve fare per i keyword arguments. Insomma una firma come la seguente:

def metodo (param1, param2...., *resto)

è adatta allo scopo. Vediamo un esempio base:

  Esempio 8.6
1
2
3
4
5
6
7
def stampa (s1, s2, *s3)
puts("#{s1}, #{s1}, #{s3.inspect}")
end

stampa("uno", "due")
stampa("uno", "due", "tre")
stampa("uno", "due", "tre", "quattro", "cinque")

Che cosa è dunque s3? basta stampare s3.class e ne risulterà la seguente parolina: array. In questo array vengono memorizzati gli elementi aggiunti dopo i parametri normali. Un array è una sequenza di oggetti quindi possiamo mettere tutto quello che vogliamo in esso. E' anche possibile estrarre i valori col consueto operatore [] e lavorare internamente al metodo esattamente come se si avesse a che fare con un array. Se non viene passato alcun argomento l'array risulta vuoto, semplicemente. Vedremo quando avremo parlato delle classi, usi ulteriori che si possono fare di questa importante proprietà. Come ultima nota, ricordate che solo un parametro può essere dotato del prefisso *. E' interessante notare che il nostro asterisco può essere usato anche nell'ambito del richiamo di un metodo per passare ad esso un array di modo che ogni elemento dello stesso sia visto come un parametro indipendente. L'esempio seguente chiarisce in modo semplice:

  Esempio 8.7
1
2
3
4
5
6
7
8
9
def fn(a,b,c)
puts(a)
puts(b)
puts(c)
puts(a+b+c)
end

x = [1,2,3]
fn(*x)

Ogni elemento dell'array va a riempire uno dei parametri richiesti dalla firma del metodo fn. Nel caso in cui invece toglieste l'asterisco alla riga 9 ne ricavereste un errore:

fn': wrong number of arguments (1 for 3) (ArgumentError)

questo perchè l'unico parametro ad essere soddisfatto sarebbe a nel quale si troverebbe rimappato l'intero array. Ovviamente errore analogo si avrebbe anche se l'array dichiarato alla riga 8 fosse costituito da meno o più di 3 elementi.

Visto che possiamo passare un numero variabile di parametri dobbiamo anche prendere in considerazione che la possibilità che questi siano numericamente pari a zero. Ruby in questo caso ci offre una possibilità, già accennata in passato, che incontriamo nel seguente esempio:

irb(main):002:0> x = [1,2,3]
=> [1, 2, 3]
irb(main):003:0> x.length
=> 3
irb(main):004:0> x.length()
=> 3

Il punto è che in questi casi, quindi se non passiamo alcun parametro al metodo, è possibile tralasciare le parentesi. Nell'esempio length, nella prima chiamata, sembra una proprietà dell'array mentre viene mostrato subito dopo che si tratta di un metodo. La prima tipologia di scrittura è più sintetica e veloce la seconda è, probabilmente, più da puristi in quanto evidenzia la vera natura di length. Tra l'altro in Ruby l'unico modo per parlare con le classi è invocare i loro metodi, come vedremo. In questi casi è solo una questione di gusti visto che non ci sono conseguenze in caso di omissione delle parentesi. Lo stesso discorso vale per puts, tanto per fare un altro esempio di un metodo ben conosciuto:

irb(main):005:0> puts("ciao")
ciao
irb(main):006:0> puts "ciao"
ciao

Chiaro no? Anche qui nessun problema. In alcuni casi però sono necessari per risolvere delle ambiguità o per specificare esattamente cosa vogliamo fare; si veda l'esempio che segue:

  Esempio 8.8
1
2
3
4
5
6
7
def somma(a = 5, b)
a + b
end

puts somma 2,2
puts somma(2,2)
puts somma(2),2

In questo caso la parentesi è ben necessaria e definisce un comportamente diverso tra le chiamate alla riga 5 e 6 e quella alla7. L'output infatti è:

4
4
7
2

Quindi attenzione che le parentesi possono sempre servire; usatele, se avete qualche dubbio.

Parlandi degli iteratori abbia introdotti i blocchi. Questi sono in pratica dei "pacchetti" di istruzioni del tutto anonimi e che sono richiamati all'interno di un metodo tramite una keyword, ricordate? quella famosa yield che funzionava così bene per creare iterazioni custom. Ebbene, è possibile integrare la chiamata ad un certo blocco attraverso il suffisso & (ampersand) insieme al quale facciamo lavorare la keyword call. Vediamo un esempio:

  Esempio 8.9
1
2
3
4
5
6
def fn(a,b,c, &x)
puts(a+b+c)
x.call("fine")
end

fn(1,2,3){|x| puts x}

Come si vede la firma del metodo comprende anche un riferimento al blocco che troviamo alla riga 6. L'esecuzione di questo è attuata alla riga 3 tramite il metodo call. I parametri di chiamata del metodo restano invariati. Tecnicamente parlando grazie a & il nostro blocco anonimo (che deve stare sulla stessa riga della funzione, come fosse un normale parametro) diventa un oggetto Proc. Questo è un vero e proprio oggetto che rappresenta blocco. Tale oggetto può essere istanziato direttamente e lo vedremo quando ci occuperemo direttamente di esso. Da l punto di vista posizionale il richiamo tramite & nella firma del metodo va messo rigorosamente in coda rispetto agli altri parametri.

Ogni metodo in Ruby restituisce un valore. A meno di altre indicazioni esso è il valore dell'ultima espressione presente nel metodo stesso. Questo favorisce la possibilità di essere assegnata ad una qualsiasi variabile. Quindi un metodo tipo:

def a
x = 0
y = 1
end

restituisce il valore 1. Nel caso in cui non vi fosse apparentemente nessun valore di ritorno, come ad esempio nel caso di un metodo siffatto:

def pr
puts("ciao")
end

in realtà ci ritroveremmo tra le mani il classico nil che ovviamente potrebbe essere senza problemi attribuito ad una variabile, ovvero anche in questo caso è lecito scrivere cose come:
x = pr

Su un testo relativo al linguaggio si legge, stranamente, che i metodi in Ruby possono restituire un solo valore... ma non è vero! Esiste infatti la keyword return che permette appunto di restituire più di un valore: formalmente si ha:

def nome_metodo
istruzioni
return valore-1, valore-2, ...., valore-n
end

  Esempio 8.10
1
2
3
4
5
6
7
def aa
x = 1
y = 2
return x, y
end

print(aa)

L'output è il seguente:

[1, 2]

In pratica ci viene restituito un array che contiene tutti i valori indicati da return. Questa keyword interrompe l'esecuzione del metodo e restituisce il controllo al chiamante per cui bisogna evitare di mettere ulteriori istruzioni successivamente ad essa in quanto non sarebbero eseguite. Evidentemente è possibile associare il valore di return ad una variabile e gestirla normalmente come fosse un array.

  Esempio 8.11
1
2
3
4
5
6
7
8
9
def aa
x = 1
y = 2
return x, y
end

x = aa
puts(aa.class)
print(x[0], " ", x[1])

Che ha il seguente chiaro output:

Array
1 2

Prima di chiudere questa parte illustriamo una istruzione abbastanza curiosa ma utile in qualche caso per risulvere magari situazione di conflitto; si tratta di undef che in pratica libera il nome del metodo e lo rende disponibile per altri usi. E' importante tenere presente però che da quel momento in avanti, sequenzialmente parlando,  il metodo non sarà più utilizzabile nel programma, a meno che non venga ridefinito; ricordatevi sempre che nel dubbio che quel metodo, o meglio quella particolare sequenza di istruzioni racchiusa nel metodo, vi serva potete sempre ricavarvene una copia di sicurezza tramite alias, come abbiamo visto. Ecco un semplice esempio completo di quanto abbiamo detto:

  Esempio 8.12
1
2
3
4
5
6
7
8
9
def saluto
puts("ciao")
end

saluto
alias saluto_2 saluto
undef saluto
saluto_2
saluto

Che ci presenta il seguente output:

ciao
ciao
0093.rb:9:in `<main>': undefined local variable or method `saluto' for main:Obje
ct (NameError)

La riga di errore evidenziata è conseguenza del nostro undef utilizzato alla riga 7.