Ruby Deep Dive: metodi
L’episodio 2 ha coperto if, case e i loop — i costrutti che decidono cosa viene eseguito. Questo episodio parla di ciò che viene effettivamente deciso di eseguire: i metodi. Non ancora le classi (quello è territorio dell’episodio 4) — solo un metodo standalone, i suoi argomenti, e cosa restituisce a chi lo ha chiamato.
Il codice è nel repo ruby-deep-dive, taggato episode-3. Stesso formato di sempre: leggi il post, poi completa 03-methods/exercises.rb finché 03-methods/exercises_test.rb non diventa tutto verde.
Definire un metodo
def add(a, b)
a + b
end
def, un nome, parentesi intorno ai parametri (facoltative, ma vale la pena scriverle sempre), un corpo, end. Nessuna dichiarazione di tipo da nessuna parte — non su a, non su b, non su cosa add restituisce. È la stessa idea dell’episodio 1 — “un nome è solo un’etichetta, non una scatola con un tipo dichiarato” — applicata ai parametri di metodo: a e b sono solo nomi che vengono legati a qualunque oggetto sia stato passato, di qualunque tipo esso sia.
Il valore di ritorno: implicito ed esplicito
add non ha la keyword return, eppure restituisce comunque a + b. Un metodo Ruby restituisce sempre il valore dell’ultima espressione valutata nel suo corpo — è la stessa regola stabilita nell’episodio 2 per if, estesa a tutto il metodo. Non c’è nulla di speciale in return che fa “restituire qualcosa” a un metodo — ogni metodo restituisce sempre qualcosa, che tu scriva la keyword o no.
return serve per il caso in cui devi restituire un valore prima di arrivare alla fine del metodo — un’uscita anticipata:
def safe_divide(a, b)
return nil if b.zero?
a / b.to_f
end
Leggilo così: “se b è zero, fermati qui e restituisci nil; altrimenti continua, e qualunque cosa valuti l’ultima riga è la risposta.” Il return sulla prima riga è esplicito e necessario — senza, nil sarebbe solo un valore in mezzo al metodo, scartato. La a / b.to_f sull’ultima riga non ha bisogno di return — è già esattamente dove la regola del ritorno implicito va a cercare un valore. Il Ruby idiomatico si appoggia continuamente a questo: usa return esplicito per le uscite anticipate e le guard clause, e lascia che l’ultima riga faccia il resto ovunque altro. Un return sull’ultimissima riga di un metodo è legale ma ridondante — lo vedrai raramente nel codice Ruby.
Argomenti posizionali e con default
add(a, b) e greet(name) prendono entrambi argomenti posizionali — il primo valore del chiamante riempie il primo parametro, il secondo il secondo, abbinati puramente per posizione, non per nome. Dai a un parametro un valore di default, e il chiamante può ometterlo:
def greet(name, greeting = "Hello")
"#{greeting}, #{name}!"
end
greet("Ada") #=> "Hello, Ada!"
greet("Ada", "Hi") #=> "Hi, Ada!"
greeting = "Hello" non viene eseguito al momento della definizione del metodo — viene eseguito ogni volta che il metodo è chiamato senza quell’argomento, quindi l’espressione di default può persino fare riferimento a parametri precedenti o chiamare altri metodi; viene valutata di nuovo a ogni chiamata, non calcolata una volta sola e riusata.
Argomenti keyword
Gli argomenti posizionali hanno un vero punto debole appena un metodo ne prende più di due o tre: al punto di chiamata, describe_person("Ada", 36) non ti dice nulla su quale numero sia cosa senza andare a controllare la definizione del metodo. Gli argomenti keyword risolvono il problema nominando il valore proprio dove viene passato:
def describe_person(name:, age: 0)
"#{name} (#{age})"
end
describe_person(name: "Ada") #=> "Ada (0)"
describe_person(name: "Ada", age: 36) #=> "Ada (36)"
describe_person(age: 36, name: "Ada") #=> "Ada (36)" -- l'ordine non conta
name: senza nulla dopo i due punti nella lista dei parametri significa “obbligatorio — chiamare questo metodo senza un argomento name: è un ArgumentError, non un nil.” age: 0 significa “facoltativo, con default 0” — esattamente la stessa idea di valore di default degli argomenti posizionali, solo scritta con una keyword. Due scelte indipendenti — posizionale vs. keyword, e obbligatorio vs. con default — che si combinano liberamente. Usa argomenti keyword nel momento in cui la lista dei parametri di un metodo supera uno o due valori, o quando due parametri sono dello stesso tipo e potrebbero essere scambiati silenziosamente da chi chiama (due interi, due stringhe) — la keyword rende quell’errore impossibile da scrivere per sbaglio.
Splat: catturare il resto
A volte un metodo genuinamente non sa in anticipo quanti argomenti riceverà. * in una lista di parametri — lo splat — raccoglie ogni argomento posizionale rimanente in un Array:
def sum_all(*numbers)
numbers.sum
end
sum_all(1, 2, 3) #=> 6
sum_all() #=> 0
sum_all(5) #=> 5
Dentro il metodo, numbers è semplicemente un Array normale, che il chiamante abbia passato zero, uno o cinquanta argomenti — non c’è nessun tipo speciale “argomenti variadici” da imparare, lo splat li impacchetta semplicemente nello stesso Array che già conosci dal materiale Tier-1 dell’episodio 1. Il doppio splat, **, fa l’equivalente per gli argomenti keyword — raccoglie ogni argomento keyword passato dal chiamante in un Hash:
def build_profile(**attributes)
attributes
end
build_profile(name: "Ada", age: 36) #=> { name: "Ada", age: 36 }
* e ** non servono solo a definire metodi flessibili — funzionano anche al contrario, al punto di chiamata, per espandere una collezione che già hai in argomenti separati: sum_all(*[1, 2, 3]) è esattamente la stessa chiamata di sum_all(1, 2, 3).
Splat a sinistra: assegnazione destrutturante
Lo stesso * compare in un posto che non è affatto una definizione di metodo — la semplice assegnazione:
first, *rest = [1, 2, 3, 4]
first #=> 1
rest #=> [2, 3, 4]
Questa si chiama assegnazione destrutturante (o “multipla”): gli elementi del lato destro vengono distribuiti tra i nomi a sinistra, e *rest assorbe tutto ciò che avanza in un Array — vuoto se non resta nulla, mai nil:
def first_and_rest(array)
first, *rest = array
[first, rest]
end
first_and_rest([1, 2, 3]) #=> [1, [2, 3]]
first_and_rest([5]) #=> [5, []]
first_and_rest([]) #=> [nil, []]
first_and_rest([]) restituisce [nil, []], non un errore — destrutturare un array vuoto lascia semplicemente first non assegnato (nil), esattamente come qualunque variabile locale è nil prima di essere mai impostata.
Convenzioni di naming: ? e !
Due suffissi sono convenzioni che Ruby non impone da nessuna parte nel linguaggio stesso, ma che ogni rubysta si aspetta vengano rispettate:
def palindrome?(str)
cleaned = str.downcase
cleaned == cleaned.reverse
end
Un metodo che finisce in ? è una promessa, non una regola che l’interprete controlla: questo metodo restituisce un booleano, e non cambia nulla. empty?, nil?, even? — tutti metodi predefiniti che seguono la stessa convenzione. Niente ti impedisce di scrivere def valid?; @errors = []; true; end, ma farlo viola una convenzione su cui ogni lettore del tuo codice fa affidamento, silenziosamente, dando per scontato che sia vera.
def reverse_in_place!(array)
array.reverse!
end
Un metodo che finisce in ! è l’altra promessa: questo metodo muta qualcosa a cui il chiamante aveva già un riferimento, invece di costruire e restituire qualcosa di nuovo. Questo si ricollega direttamente all’idea dell’episodio 1 — “una variabile è un’etichetta, non una scatola” — array.reverse! non crea un nuovo array invertito e lo restituisce; inverte lo stesso oggetto Array sul posto. Anche la variabile originale del chiamante vede il cambiamento, perché ha sempre puntato a quell’unico oggetto:
array = [1, 2, 3]
result = reverse_in_place!(array)
result.equal?(array) #=> true -- letteralmente lo stesso oggetto, non solo valori uguali
! non significa tecnicamente “pericoloso” o “muta” secondo nessuna regola imposta dal linguaggio (Kernel#exit!, per esempio, non muta nulla) — la versione più precisa in una riga è “questo metodo fa qualcosa su cui dovresti riflettere due volte prima di chiamarlo,” e “muta il suo argomento o receiver sul posto” è di gran lunga la ragione più comune per cui succede. La regola pratica che regge davvero: quando un metodo ha sia una versione normale che una versione ! (sort/sort!, reverse/reverse!, upcase/upcase!), quella normale restituisce un nuovo oggetto e lascia stare l’originale, e quella ! cambia direttamente l’oggetto originale.
Gli esercizi
Nove metodi in 03-methods/exercises.rb: ritorno implicito, return esplicito per l’uscita anticipata, un argomento con default, argomenti keyword obbligatori e con default, uno splat che raccoglie argomenti posizionali, un doppio splat che raccoglie argomenti keyword in un Hash, un metodo predicato ?, un metodo mutante !, e assegnazione destrutturante con uno splat.
git clone https://github.com/AntoninoScaffidi/ruby-deep-dive.git
cd ruby-deep-dive
bundle install
ruby 03-methods/exercises_test.rb
Cosa viene dopo
L’episodio 4 passa dai metodi standalone alle classi e agli oggetti: initialize, variabili di istanza, e attr_accessor — il punto in cui “un metodo” comincia a diventare “un metodo che appartiene a qualcosa.”