Created: 2018-12-06 gio 13:19
Questo documento è disponibile in formato pdf.
Questa presentazione ed il file pdf sono prodotti da un unico sorgente in org-mode.
Il linguaggio di programmazione Ruby è stato progettato da Matsumoto Yukihiro e la prima versione considerata stabile risale al 1996.
Ruby è un linguaggio generico interpretato, dinamico, riflessivo e multi-paradigma.
Matsumoto afferma di essere stato influenzato dalle idee del Perl, Smalltalk e Lisp di cui quindi coesistono le caratteristiche.
Un modello ad oggetti puro (ogni elemento è un oggetto), caratteristiche funzionali e praticità imperativa.
Lo spirito che avrebbe spinto Matsumoto nell'ideazione di Ruby si può assimilare al principio della minima sorpresa anche se come da lui affermato era il principio della sua minima sorpresa.
La sintassi è simile a quella del Perl e del Python.
Le classi ed i metodi sono dichiarati con delle parole chiave, mentre i blocchi di codice ne hanno una doppia possono essere racchiusi tra parentesi o parole chiave.
Le variabili hanno quattro possibili aree di visibilità:
$
: $variabile
@@
: @@variabile
@
: @variabile
variabile
Le variabili di istanza e le variabili di classe sono completamente private ed hanno bisogno di getter e setter ma il linguaggio offre dei metodi di convenienza per la generazione dinamica di questi metodi:
Spazi ed indentazione non sono significanti come nel Python.
Ruby è un linguaggio orientato agli oggetti puro ed ogni valore, comprese le primitive ed il nil, è un oggetto.
Il codice è valutato ed istanziato alla lettura per cui la stessa dichiarazione di classe è di per sé un oggetto.
Le funzioni sono metodi nel senso comune dei linguaggi orientati agli oggetti e sono invocate sempre su un oggetto. Quelle dichiarate al di fuori di classi o moduli entrano a far parte del genitore principale (Object
) diventando di fatto globali. I metodi (come gli altri blocchi esecutivi) restituiscono l'ultimo valore assegnato relegando la parola chiave return
ad utilizzi specifici e limitati.
L'ereditarietà è singola ma ogni classe può importare più moduli con un meccanismo di mixin.
Il concetto di tipo segue la regola del duck typing soffermandosi più sul cosa fa un oggetto che piuttosto sul cosa è. Di fatto un oggetto è i suoi metodi e la risoluzione si limita alla domanda: sei in grado di rispondere a questo messaggio? In caso affermativo la computazione procederà, altrimenti verrà emessa una eccezione NoMethodError
a meno che non sia implementato un proxy attraverso method_missing
che è invocato nel caso la risoluzione non vada a buon fine.
1: class Pippo 2: 3: def method_missing(name, *args, &block) 4: puts "method: #{name} arguments: #{args.join(',')}" 5: end 6: 7: end 8: 9: pippo = Pippo.new 10: 11: pippo.m(1, 2, 3)
Ruby è anche un linguaggio funzionale seppure impuro considerando che:
1: somma = lambda { | a, b | a + b } 2: 3: def applica_la_funzione_alla_lista(f, lista) 4: lista.reduce(&f) 5: end 6: 7: puts applica_la_funzione_alla_lista(somma, [1, 2, 3, 4, 5]) #=> 15
Equivalente a questo sorgente:
1: somma = ->(a, b) { a + b } #Nuova sintassi per le lambda 2: 3: def applica_la_funzione_alla_lista(f, lista) 4: lista.reduce(&f) 5: end 6: 7: puts applica_la_funzione_alla_lista(somma, [1, 2, 3, 4, 5]) #=> 15
Esistono diverse implementazioni del linguaggio Ruby:
Installare Ruby non è cosa complessa, sulle maggiori distribuzioni Gnu/Linux è presente nei repository ufficiale (anche se spesso non nell'ultima versione) e va fatto riferimento al gestore di pacchetti della distribuzione specifica.
Nello sviluppo ma soprattutto nella distribuzione di applicazioni Ruby sono utili dei veri e propri manager. I più usati sono:
Rvm è il più completo ma anche il più invasivo. Uru il più semplice e più multipiattaforma.
Informazioni sull'installazione: https://www.ruby-lang.org/en/documentation/installation/
L'interprete Ruby è un eseguibile dal nome ruby il cui uso è:
ruby [switches] [--] [programfile] [arguments]
Per evidenziare la lista completa delle opzioni da linea di comando: ruby --help
.
L'interprete mette a disposizione una serie di variabili predefinite tra cui:
Alcune variabili predefinite (possono essere mutate)
1: puts "Nome delle script: #{$0}" 2: puts "Argomenti della linea di comando: #{$*}" 3: puts "Il percorso di caricamento per gli script: #{$:}" 4: puts "Il percorso di caricamento per gli script: #{$LOAD_PATH}"
Alcune costanti predefinite
1: puts "La versione di Ruby: #{RUBY_VERSION }" 2: puts "Data di rilascio: #{RUBY_RELEASE_DATE }" 3: puts "La piattaforma: #{RUBY_PLATFORM }" 4: puts "Un dizionario che contiene le variabili di ambiente: #{ENV.keys.join(',')}"
Per consultare la lista completa: https://ruby-doc.org/core-2.5.3/doc/globals_rdoc.html
La shell interattiva di Ruby è una REPL (read-eval-print loop) il cui nome eseguibile è irb.
Permette di valutare immediatamente espressioni e comandi nel linguaggio permettendo di sperimentare in real-time. È comunque possibile utilizzare direttamente l'interprete al prompt dei comandi:
1: nissl:code ➜ ruby 2: 3: def stampa(t) 4: puts t 5: end 6: 7: stampa("hello world") 8: 9: ^D 10: 11: hello world 12: 13: nissl:code ➜ 14:
Irb ha capacità di mantenere una storia dei comandi dati e una rudimentale capacità di editing delle linee:
1: nissl:code ➜ irb 2: irb(main):001:0> def stampa(t) 3: irb(main):002:1> puts t 4: irb(main):003:1> end 5: => :stampa 6: irb(main):004:0> stampa("hello irb") 7: hello irb 8: => nil 9: irb(main):005:0>
Una REPL più avanzata di irb
è pry
(http://pryrepl.org/) che oltre alla normale funzionalità offerta da quella standard dispone alcune caratteristiche interessanti tra cui:
ls
, cd
e altri comandi1: 2: nissl:code ➜ pry 3: [1] pry(main)> def stampa(t) 4: [1] pry(main)* puts t 5: [1] pry(main)* end 6: => :stampa 7: [2] pry(main)> stampa("hello world") 8: hello world 9: => nil
1: [3] pry(main)> cd Hash 2: [4] pry(Hash):1> ls 3: Object.methods: yaml_tag 4: Hash.methods: [] try_convert 5: Hash#methods: 6: < clear delete fetch invert pretty_print shift transform_keys 7: <= compact delete_if fetch_values keep_if pretty_print_cycle size transform_keys! 8: == compact! dig flatten key rassoc slice transform_values 9: > compare_by_identity each has_key? key? rehash store transform_values! 10: >= compare_by_identity? each_key has_value? keys reject to_a update 11: [] default each_pair hash length reject! to_h value? 12: []= default= each_value include? member? replace to_hash values 13: any? default_proc empty? index merge select to_proc values_at 14: assoc default_proc= eql? inspect merge! select! to_s 15: locals: _ __ _dir_ _ex_ _file_ _in_ _out_ _pry_ 16: [5] pry(Hash):1> show-method each 17: 18: From: hash.c (C Method): 19: Owner: Hash 20: Visibility: public 21: Number of lines: 10 22: 23: static VALUE 24: rb_hash_each_pair(VALUE hash) 25: { 26: RETURN_SIZED_ENUMERATOR(hash, 0, 0, hash_enum_size); 27: if (rb_block_arity() > 1) 28: rb_hash_foreach(hash, each_pair_i_fast, 0); 29: else 30: rb_hash_foreach(hash, each_pair_i, 0); 31: return hash; 32: } 33: [6] pry(Hash):1> 34:
__ENCODING__ __LINE__ __FILE__ BEGIN END alias and begin break case class def defined? do else elsif end ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield
Alcune parole chiave hanno dei corrispondenti operatori con precedenza più elevata:
&& > and
|| > or
! > not
Le espressioni letterali creano od inizializzano oggetti e coprono i vari tipi di base:
nil, Booleani, Numeri, Stringhe, Simboli, Arrays, Hashes, Serie, Espressioni regolari, Procs
Sia nil
che false
vengono ambedue valutati come false
nei test. Il valore nil
equivale al null
di altri linguaggi di programmazione ma in sé è una istanza della classe NilClass
. In Ruby tutto è un oggetto e non va dimenticato.
Ogni oggetto che non sia nil
o false
è valutato true
nelle espressioni condizionali.
1: nil.class 2: #=>NilClass 3: false.class 4: #=>FalseClass 5: true.class 6: #=>TrueClass
Nei numeri si può usare il carattere _
(underscore) come separatore per aumentarne la leggibilità: sia 1234
che 1_234
corrispondono a 1234
.
I numeri in virgola possono essere espressi sia con il punto come separatore decimale che in notazione esponenziale: 0.000134
o 134e-6
.
1: 1234.class 2: #=>Integer 3: 1.234.class 4: #=>Float
Per esprimere numeri in altre notazioni si usano dei prefissi: 0d
la notazione decimale, 0x
esadecimale, 0
o 0o
ottale, 0b
binaria.
La notazione più comune per dichiarare una stringa è tra doppi apici: "questa è una stringa"
.
Si può usare anche quella tra apici singoli 'questa è una stringa'
, ma in questo caso non è permessa né l'intepolazione, né i vari caratteri come \n
, \t
e così via.
Le stringhe possono essere create anche con %(questa è una stringa)
che con %q(questa è una stringa)
, che equivalgono rispettivamente alle forme con apice doppio o singolo.
Stringhe adiacenti sono automaticamente concatenate. È possibile costruire una stringa di un solo carattere con ?
.
1: 2: num = 4 3: #=>4 4: "oggi siamo in #{num}" 5: #=>"oggi siamo in 4" 6: 'oggi siamo in #{num}' 7: #=>"oggi siamo in \#{num}" 8: %(oggi siamo in #{num}) 9: #=>"oggi siamo in 4" 10: %q(oggi siamo in #{num}) 11: #=>"oggi siamo in \#{num}" 12: "#{num}" "a" '1' "b" "c" 13: #=>"4a1bc" 14: ?X 15: #=>"X" 16:
Come in altri linguaggi sono supportate le stringhe lunghe o here document con alcune modalità. Il marcatore di inizio e fine può essere qualunque e per convenzione si usa tutto maiuscolo.
1: s1 = <<HD 2: - Il terminale deve essere allineato. - 3: sono una stringa 4: lunga 5: HD 6: 7: s2 = <<-HD 8: - Il terminale può essere indentato. - 9: sono una stringa 10: lunga 11: HD 12: 13: s3 = <<~HD 14: - Il terminale può essere indentato e la stringa viene allineata. - 15: sono una stringa 16: lunga 17: HD 18: 19: puts s1 20: puts s2 21: puts s3
Altri usi interessanti delle heredoc: invocare un metodo sulla stringa e lanciare un programma esterno catturandone l'output.
La forma <<`HD`
equivale ad invocare Kernel#`
che è un metodo che lancia un programma come sottoprocesso.
1: s1 = <<HD.split 2: sono una stringa 3: lunga 4: HD 5: 6: s2 = <<`HD` 7: date 8: HD 9: 10: p s1 11: puts s2
I simboli sono dei nomi costanti all'interno dell'interprete, una volta allocati vengono recuperati. Prima della versione 2.2 tutti gli oggetti della classe Symbol permanevano per tutta la durata dell'esecuzione (portando ad alcuni problemi), oggi quelli creati durante l'esecuzione (tramite il metodo to_sym
) sono eleggibili per il garbage collector.
Hanno un uso estensivo nella programmazione Ruby specialmente come chiavi per i dizionari (Hash) dove sono (o meglio erano) essenzialmente più veloci delle stringhe non venendo riallocati ogni volta.
1: module A 2: $str_a = "pippo" 3: $sym_a = :pippo 4: end 5: 6: puts $str_a.object_id 7: 8: module B 9: $str_a = "pippo2" 10: $str_b = "pippo" 11: $sym_b = :pippo 12: end 13: 14: puts $str_a.object_id 15: puts $str_b.object_id 16: puts $sym_a.object_id 17: puts $sym_b.object_id
L'array è simile come in molti altri linguaggi ma può essere eterogeneo e si utilizzano le []
.
1: e = [1+2] 2: #=>[3] 3: c = [1 + 2, 3 + 1] 4: #=>[3, 4] 5: d = [1, [2, 3], 2] 6: #=>[1, [2, 3], 2] 7: c = [1, "a"] 8: #=>[1, "a"] 9: b = ["a", "b", "c"] 10: #=>["a", "b", "c"] 11: a = [1, 2, 3] 12: #=>[1, 2, 3]
Le espressioni regolari sono simili a quelle del Perl e vengono inizializzate in vari modi:
/espressione regolare/[flag]
%r{espressione regolare}[flag]
Se si usa la forma %r
si possono usare vari delimitatori:
1: %r[.*] 2: #=>/.*/ 3: %r!.*! 4: #=>/.*/ 5: %r .* 6: #=>/.*/ 7: %r^.*^ 8: #=>/.*/ 9: %r|.*| 10: #=>/.*/
Come si è visto con il segno %
si possono creare le stringhe o le espressioni regolari ma non solo:
1: %i(a b c d) 2: #=>[:a, :b, :c, :d] 3: %q(questa è una stringa) 4: #=>"questa è una stringa" 5: %r(.*) 6: #=>/.*/ 7: %s(simbolo) 8: #=>:simbolo 9: %w(a b c d) 10: #=>["a", "b", "c", "d"] 11: %x(echo "comando echo invocato") 12: #=>"comando echo invocato\n"
L'operatore base per il matching di una espressione regolare è =~
che più o meno equivale al metodo match
(il metodo restituisce una istanza di MatchData invece dell'indice iniziale o nil
).
1: md = /(.)(.)(\d+)(\d)/.match("Pippo123.") 2: #=> MatchData "po123" 1:"p" 2:"o" 3:"12" 4:"3" 3: first_occ_index = /(.)(.)(\d+)(\d)/ =~ "Pippo123." 4: #=>3 5: first_occ_index 6: #=>3
L'Hash
è una delle strutture probabilmente più usate in ambito Ruby.
Non ne è garantito l'ordine dichiarativo, il linguaggio cercherà in base alla struttura la migliore ottimizzazione in memoria.
L'hash è creato usando delle coppie chiave => valore tra {}
.
1: h = { "k" => 1, "k2" => "valore" } 2: #=>{"k"=>1, "k2"=>"valore"} 3: h = { "k":1, "k2": "valore" } 4: #=>{:k=>1, :k2=>"valore"} 5: h = { k: 1, k2: "valore" } 6: #=>{:k=>1, :k2=>"valore"} 7: h = { :k => 1, :k2 => "valore" } 8: #=>{:k=>1, :k2=>"valore"} 9: h = { :k => 1, :k2 => "valore", a: [1, 2, 3], h: {1 => "1", 2 => "2" }} 10: #=>{:k=>1, :k2=>"valore", :a=>[1, 2, 3], :h=>{1=>"1", 2=>"2"}}
Come chiave può essere usato praticamente qualunque valore, visto che la chiave stessa è un oggetto.
1: h = {[1,2,3] => "chiave come array"} 2: #=>{[1, 2, 3]=>"chiave come array"} 3: h = {{a: 1, b: 2} => "chiave come hash"} 4: #=>{{:a=>1, :b=>2}=>"chiave come hash"} 5: h = {(1..2) => "chiave come range"} 6: #=>{1..2=>"chiave come range"} 7: h = { ->(a, b) { a + b } => "chiave come proc/lambda" } 8: #=>{#<Proc:0x00007ff2d660bf00@(pry):22 (lambda)>=>"chiave come proc/lambda"}
Fino alla versione 2.2 di Ruby era consigliabile per motivi prestazionali usare i simboli come chiavi che come detto sopra sono allocati una sola volta. Con la versione 2.2 le stringhe come chiavi sono state ottimizzate e la differenza di prestazione è minima (anche se sempre a favore dei simboli).
I Range sono intervalli di valori, l'estremo finale può essere incluso od escluso. Anche qui si possono creare intervalli di qualsiasi valore.
1: (1..5) 2: #=>1..5 3: (1...5) 4: #=>1...5 5: (1...5).to_a 6: #=>[1, 2, 3, 4] 7: (1..5).to_a 8: #=>[1, 2, 3, 4, 5] 9: ('a'..'z').to_a 10: #=>["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
Un intervallo può contenere qualsiasi oggetto che risponda all'operatore <=>
e se usato come una enumerazione a succ
. <=>
ha solo tre possibili valori di ritorno: -1
, 0
, 1
.
a <=> b
, se a < b
allora -1a <=> b
, se a = b
allora 0a <=> b
, se a > b
allora 1nil
Una classe di esempio da usare all'interno di un range.
1: class StrOfLetter 2: # si include (mixin) il modulo Comparable per avere gli operatori 3: # >, < , >=, <=, ==, and between? gratuitamente 4: include Comparable 5: attr :lenght 6: 7: def initialize(lenght) 8: @letter = 'A' 9: @lenght = lenght 10: end 11: 12: def <=>(other) 13: @lenght <=> other.lenght 14: end 15: 16: def succ 17: StrOfLetter.new(@lenght + 1) 18: end 19: 20: end 21: 22: range = (StrOfLetter.new(1)..StrOfLetter.new(5)) 23: p range.to_a
Arricchiamo la classe, aggiungendo inspect
che ritorna una rappresentazione umana dell'oggetto e to_s
.
1: class StrOfLetter 2: include Comparable 3: attr :lenght 4: 5: def initialize(lenght) 6: @letter = 'A' 7: @lenght = lenght 8: end 9: 10: def <=>(other) 11: @lenght <=> other.lenght 12: end 13: 14: def succ 15: StrOfLetter.new(@lenght + 1) 16: end 17: 18: def to_s 19: value = inspect 20: "word: #{value} size: #{value.size}" 21: end 22: 23: def inspect 24: @letter * @lenght 25: end 26: end 27: 28: range = (StrOfLetter.new(1)..StrOfLetter.new(5)) 29: p range.to_a 30: p range.to_a[1].to_s
Le proc sono funzioni anonime. In Ruby le lambda e le proc hanno delle sottili differenze:
lambda {}
o ->{}
, agiscono come funzioni in sé, chiamando return
all'interno del blocco escono semplicemente.Proc.new {}
o proc {}
, agisce come parte del metodo chiamante, uscire con return
fa uscire anche il chiamante.Vediamo le differenze.
1: @lmbd1 = lambda {return "sono una lambda 1"} 2: @lmbd2 = ->{return "sono una lambda 2"} 3: @proc = Proc.new {return "sono una proc"} 4: 5: def call_lmbd 6: puts "calling lambda..." 7: puts @lmbd1.call 8: puts @lmbd2.call 9: puts "continuo..." 10: end 11: 12: def call_proc 13: puts "calling proc..." 14: puts @proc.call 15: puts "continuo..." 16: end 17: 18: call_lmbd 19: call_proc
Essendo funzioni anonime possono accettare parametri ed anche qui ci sono differenze:
lambda {}
o ->{}
, richiedono che i parametri siano rispettati e permettono quelli di default.Proc.new
o proc {}
, non richiedono di rispettare i parametri.1: l = ->(a, b = 1) { a + b } 2: #=> Proc:0x00007ff2d63dccc0@(pry):65 (lambda) 3: l.call(3) 4: #=>4 5: l.call 6: ArgumentError: wrong number of arguments (given 0, expected 1..2) 7: from (pry):65:in `block in __pry__' 8: pr = Proc.new {|a, b| a + b } 9: #=> Proc:0x00007ff2d5462b28@(pry):68 10: pr.call 11: NoMethodError: undefined method `+' for nil:NilClass 12: from (pry):68:in `block in __pry__' 13: pr = Proc.new {|a, b| 1 + 1 } 14: #=> Proc:0x00007ff2d54dd670@(pry):70 15: pr.call 16: #=>2
freeze
Per essere considerato una costante il nome di un oggetto deve iniziare per lettera maiuscola. Sono così costanti i nomi delle classi e dei moduli. Per convenienza le costanti che contengono solo valori sono dichiarate tutto in maiuscolo.
Le costanti in Ruby in realtà sono mutabili e questo può creare confusione, il linguaggio si limita ad emettere un avvertimento in caso di sostituzione ma ignora in altri casi.
1: COSTANTE = 1 2: #=> 1 3: COSTANTE = 2 4: warning: already initialized constant COSTANTE 5: warning: previous definition of COSTANTE was here 6: #=> 2 7: 8: NAME = "hello" 9: #=> "hello" 10: NAME << "world" 11: #=> "helloworld"
Per ovviare a questo possiamo usare il metodo freeze
.
1: FREEZED = "hello".freeze 2: #=> "hello" 3: FREEZED << "world" 4: #FrozenError: can't modify frozen String
Attenzione che però freeze
congela ed ha senso solo su oggetti considerati mutabili come stringhe o array, solo l'istanza su cui è invocato è congelata.
1: a = [1, 2, 3] 2: #=> [1, 2, 3] 3: a.freeze 4: #=> [1, 2, 3] 5: a << 4 6: FrozenError: can't modify frozen Array 7: 8: a = [1, 2, 3, 4] #riassegnando si crea un nuovo oggetto non congelato 9: #=> [1, 2, 3, 4] 10: a << 4 11: #=> [1, 2, 3, 4, 4]
È stato annunciato che Ruby 3 avrà le stringhe immutabili di default.
In Ruby più o meno tutto è un'espressione e quindi in qualche modo restituirà un valore. L'espressione condizionale if-else-end
restituirà il valore assegnato per ultimo. Come scorciatoia è presente anche l'operatore ternario ?:
.
L'espressione unless-else-end
equivale a if not true-else-end
.
1: a = 10 2: #=> 10 3: ret = if a > 9 4: "a è maggiore di nove" 5: else 6: "a non è maggiore di nove" 7: end 8: #=> "a è maggiore di nove" 9: ret 10: #=> "a è maggiore di nove" 11: 12: ret = a > 9 ? "vero" : "falso" 13: #=> "vero"
Ci sono le classiche strutture per i cicli come for value in range do-end
, while-end
o begin-end-while
, until-do-end
o begin-end-until
ma sono veramente poco utilizzare in Ruby preferendo altri metodi.
1: for n in 1..5 do 2: puts "questo loop ciclerà #{n} volte" 3: end 4: #=> questo loop ciclerà 1 volte 5: #=> ... 6: #=> 1..5
1: x = 5 2: #=> 5 3: while x > 0 4: x -= 1 5: puts "questo loop ciclerà #{x} volte" 6: end 7: #=> questo loop ciclerà 4 volte 8: #=> ... 9: #=> nil
Chi programma in Ruby preferisce invece l'iterazione attraverso i vari metodi delle collezioni.
1: (1..5).each do |n| 2: puts "questo loop ciclerà #{n} volte" 3: end 4: #=> questo loop ciclerà 1 volte 5: #=> ... 6: #=> 1..5 7: 8: 5.times.each { |n| puts "questo loop ciclerà #{n} volte" } 9: #=> ...
Il blocco do-end
si può scrivere anche con {}
ed è in effetti una proc, quindi il metodo each
accetta come parametro una proc che chiama iterativamente per ogni elemento della collezione.
Possiamo scrivere una personale versione di each
1: def my_each(coll, &block) 2: for e in coll do 3: block.call(e) 4: end 5: end 6: 7: my_each(1..5) { |n| 8: puts "questo loop ciclerà #{n} volte" 9: }
Il parametro &block
è la proc che passeremo alla funzione e che sarà invocata nel ciclo sulla collezione.
I metodi sono definiti attraverso la parola chiave def
, hanno un nome ed eventualmente dei parametri. Restituiscono l'ultimo valore nel blocco esecutivo rendendo di fatto relegata ad usi speciali la parole return
. Si tende a non forzare mai l'uscita dal metodo nel mezzo di una computazione.
1: def met(par1, par2, par3) 2: puts par1 3: puts par2 4: puts par3 5: end 6: met(1,2,3) 7: #=> 1 8: #=> 2 9: #=> 3 10: #=> nil
Sono possibili parametri di default che non necessariamente dovranno essere in cima o in fondo alla lista quanto piuttosto raggruppati insieme.
1: def met(par1 = 1, par2 = 2, par3) 2: puts par1 3: puts par2 4: puts par3 5: end 6: met(3) 7: #=> 1 8: #=> 2 9: #=> 3 10: #=> nil
Dichiarare il metodo come def met(par1 = 1, par2, par3 = 3)
genererà un errore sintattico.
Il linguaggio supporta oltre agli argomenti posizionali anche quelli nominali che seguono le stesse regole di quelli di default.
1: def met(primo:, secondo: 2) 2: primo + secondo 3: end 4: 5: met(primo: 1) 6: #=> 3
Sono possibili numeri variabili di argomenti ed argomenti hash.
1: def met(*args) 2: p args 3: end 4: 5: met(1,2,3) 6: [1, 2, 3] 7: #=> [1, 2, 3] 8: 9: def met2(**hash) 10: p hash 11: end 12: 13: met2(a: 1, b: 2) 14: {:a=>1, :b=>2} 15: #=> {:a=>1, :b=>2} 16:
la decomposizione di un oggetto che risponde al metodo to_ary
come un Array.
1: 2: def met((a, b, *c)) 3: p a: a, b: b, c: c 4: end 5: 6: met([1, 2, 3, 4, 5]) 7: #=> {:a=>1, :b=>2, :c=>[3, 4, 5]} 8:
I metodi hanno sempre un parametro implicito che può essere esplicitato nella dichiarazione: il blocco. Come visto può giocare un ruolo importante nei metodi che lavorano sulle collezioni ma non solo. Il parametro blocco deve nel caso sia esplicitato essere l'ultimo della lista.
1: def met 2: yield 3: end 4: 5: met 6: #=> LocalJumpError: no block given (yield) 7: 8: met { p "sono il blocco" } 9: #=> "sono il blocco"
Per valutare o meno la presenza di un blocco implicito è disponibile block_given?
.
1: def met 2: if block_given? 3: yield 4: else 5: "nessun blocco disponibile" 6: end 7: end 8: 9: met 10: #=> "nessun blocco disponibile" 11: 12: met { p "ci sono" } 13: #=> "ci sono"
I blocchi sono delle proc (funzioni anonime) e quindi possono avere parametri come già visto nella funzione my_each
che possiamo scrivere anche:
1: def my_each(coll) 2: for e in coll do 3: yield e 4: end if block_given? 5: end 6: 7: my_each(1..5) { |n| 8: puts "questo loop ciclerà #{n} volte" 9: }
In Ruby esistono metodi che teminano con ?
, è una convenzione per indicare i metodi il cui intento è puramente di controllo booleano.
1: @variabile_non_inizializzata.nil? 2: #=> true 3: 4: [1, 2, 3].include?(2) 5: #=> true 6: 7: File.exist?("nome.txt") 8: #=> false
Oltre a questi ci sono quelli che terminano per !
, come sopra è una convenzione per indicare i metodi distruttivi cioè quelli che modificano direttamente l'oggetto su cui sono invocati. Spesso sono presenti in due versioni distruttiva e non.
1: a = [1, 1, 2, 2, 3] 2: #=> [1, 1, 2, 2, 3] 3: b = a.uniq 4: #=> [1, 2, 3] 5: a 6: #=> [1, 1, 2, 2, 3] 7: a.uniq! 8: #=> [1, 2, 3] 9: a 10: #=> [1, 2, 3]
La gestione delle eccezioni è molto semplice.
1: begin 2: 1 / 0 3: rescue 4: puts "Eccezione generica" 5: end
1: begin 2: 1 / 0 3: rescue Exception => e 4: puts e 5: end
1: begin 2: 1 / 0 3: rescue ZeroDivisionError => e 4: puts "Exception Class: #{ e.class.name }" 5: puts "Exception Message: #{ e.message }" 6: puts "Exception Backtrace: #{ e.backtrace }" 7: end
Una eccezione viene lanciata con raise
.
1: begin 2: raise ArgumentError.new("errore sugli argomenti") 3: rescue ArgumentError => e 4: puts e.message 5: end
Le eccezioni sono delle semplici classi Ruby.
1: class PermissionDeniedError < StandardError 2: 3: attr_reader :action 4: 5: def initialize(message, action) 6: super(message) 7: @action = action 8: end 9: end 10: 11: begin 12: raise PermissionDeniedError.new("Permesso negato", :delete) 13: rescue PermissionDeniedError => e 14: puts "Exception Class: #{ e.class.name }" 15: puts "Exception Message: #{ e.message }" 16: puts "Exception Action: #{ e.action }" 17: puts "Exception Backtrace: #{ e.backtrace }" 18: end
Un progetto non è mai composto di un solo file a meno che non sia banale. Ruby come praticamente tutti i linguaggi moderni, ha alcuni meccanismi per poter controllare questo aspetto.
1: load 'filename' 2: require 'filename'
Con load
o require
si caricano file esterni nello spazio di visibilità del richiedente. I file di per sé non sono moduli. La differenza tra i due è che load
è una direttiva che caricherà ogni volta il file mentre require
solo alla prima. Un uso tipico è nei web framework dove in ambiente di sviluppo è usata la load
e in produzione la require
.
I file se non hanno un percorso assoluto o relativo al chiamante verranno cercati nell'elenco dei percorsi di ricerca contenuti in $:
o $LOAD_PATH
.
Queste variabili sono manipolabili per aggiungere a runtime percorsi di ricerca.
1: $LOAD_PATH.unshift('/aaaa') unless $LOAD_PATH.include?('/aaaa')
Ruby è un linguaggio orientato agli oggetti puro dove tutto è un oggetto e tutto è valutato. Supporta l'ereditarieta a singolo genitore e la possibilità di incorporare uno o più moduli. I moduli sono principalmente delle collezioni di metodi più o meno polimorfi il cui scopo è quello di fornire un ambiente di visibilità e supportare il mixin.
Alcuni moduli sono incorporati a livello di interprete come per esempio il modulo Kernel
importato all'interno della classe Object
che è la radice di ogni oggetto in Ruby (non è proprio vero, BasicObject
è il vero antenato ma ha un uso interno o speciale come per esempio creare una nuova gerarchia indipendente). Ogni istanza o classe ha perciò accesso ai metodi del Kernel
.
Un modulo è creato tramite la parola chiave module
, mentre la classe con class
.
1: module MyModule 2: def met(v) 3: p v 4: end 5: end 6: 7: class MyClass 8: include MyModule 9: end 10: 11: k = MyClass.new 12: 13: k.met("metodo dal modulo")
1: module MyModule 2: def met(v) 3: p v 4: end 5: module MyModuleNested 6: def met2(v) 7: p "nested module: #{v}" 8: end 9: end 10: end 11: 12: class MyClass 13: include MyModule 14: include MyModule::MyModuleNested 15: end 16: 17: k = MyClass.new 18: 19: k.met("metodo dal modulo") 20: k.met2("metodo dal modulo")
::
1: CONSTANT = "sono una costante sulla radice" 2: 3: module MyModule 4: CONSTANT = "sono una costante" 5: 6: def c 7: p CONSTANT 8: end 9: 10: module MyModuleNested 11: def c_from_parent 12: p CONSTANT 13: end 14: 15: def c_from_root 16: p ::CONSTANT 17: end 18: end 19: end 20: 21: class MyClass 22: include MyModule 23: include MyModule::MyModuleNested 24: end 25: 26: k = MyClass.new 27: 28: k.c_from_parent 29: k.c_from_root
1: 2: class MyClass 3: class Pippo 4: def met 5: p "sono un metodo di Pippo" 6: end 7: end 8: 9: def pippo_new 10: Pippo.new 11: end 12: end 13: 14: k = MyClass.new 15: 16: k.pippo_new.met 17:
La classe Child
è dichiarata figlia di Parent
e ridefinisce il metodo age
che restituisce l'età. Per poter accedere al metodo del genitore si è usata la direttiva alias
prima della ridefinizione. È anche una dimostrazione di come la stessa dichiarazione di classe sia una istanza della classe Class
e non meramente una rappresentazione letterale di un tipo. Nella ridefinizione del metodo della classe figlia si può accedere a quello originale del genitore tramite super
.
1: class Parent 2: def age 3: "l'età di Parent è 56" 4: end 5: end 6: 7: class Child < Parent 8: 9: alias :parent_age :age 10: 11: def age 12: "l'età di Child è 13" 13: end 14: end 15: 16: c = Child.new 17: 18: p c.age 19: p c.parent_age
Ruby ha tre tipi di visibilità nelle classi e nei moduli: private
, protected
, public
. Il predefinito è public
, da notare che private
è simile al protected
di Java o C++. I metodi protected
e private
sono simili e sono invocabili all'interno della stessa gerarchia, gli ultimi non possono avere un ricevente nemmeno self
.
1: class Parent 2: def met_public 3: "sono pubblico" 4: end 5: 6: def met_private 7: "sono privato" 8: end 9: 10: def met_protected 11: "sono protetto" 12: end 13: 14: protected :met_protected 15: private :met_private 16: end 17: 18: class Child < Parent 19: def call_protected 20: self.met_protected 21: end 22: 23: def call_private 24: self.met_private 25: end 26: end 27: 28: c = Child.new 29: puts c.met_public 30: puts c.call_protected 31: puts c.call_private
I metodi di classe assomigliano nella pratica ai metodi statici di altri linguaggi, ma non lo sono. Non va dimenticato che la classe è una istanza della classe Class
, quindi tali metodi sono in relazione diretta alla classe ed a essa sono applicati. Sono dichiarati all'interno di un blocco class << self-end
(metaclasse) oppure come self.nome_del_metodo
.
Qui una definizione di un metodo di classe per la generazione dinamica di getter e setter.
1: class Parent 2: class << self 3: def attr_acc(s) 4: define_method("#{s}=") do |v| 5: instance_variable_set("@#{s}", v) 6: end 7: define_method("#{s}") do 8: instance_variable_get("@#{s}") 9: end 10: end 11: end 12: end 13: 14: class Child < Parent 15: attr_acc :value 16: 17: def sum(a) 18: a + @value 19: end 20: end 21: 22: c1 = Child.new 23: c1.value = 10 24: p c1.sum(10)
Il metodo di classe può essere invocato oltre che all'interno della definizione anche tramite notazione a punto sul nome della classe.
1: class Parent 2: class << self 3: def attr_acc(s) 4: define_method("#{s}=") do |v| 5: instance_variable_set("@#{s}", v) 6: end 7: define_method("#{s}") do 8: instance_variable_get("@#{s}") 9: end 10: end 11: end 12: end 13: 14: class Child2 < Parent 15: def sum(a) 16: a + @value 17: end 18: end 19: Child2.attr_acc(:value) 20: 21: c2 = Child2.new 22: c2.value = 25 23: p c2.sum(10)
In Ruby le classi sono aperte.
1: class String 2: def mio_metodo 3: puts self 4: end 5: end 6: 7: "sono un stringa".mio_metodo 8: #=> sono un stringa
Da adesso in poi ogni stringa avrà a disposizione il metodo mio_metodo
.
È una caratteristica potente che ovviamente andrebbe dosata con cura ed evitata il più possibile.
Ruby è un linguaggio multiparadigma e ha, come accennato, alcune caratteristiche dei linguaggi funzionali. È impuro considerando che i valori sono mutabili in maniera predefinita ed addirittura le costanti (alla versione 2.5.x) sono modificabili pur emettendo degli avvertimenti. Si è accennato al fatto che chi programma in Ruby non faccia molto uso di cicli imperativi ma preferisca tutta una serie di metodi delle collezioni che in qualche modo accettino funzioni da applicare.
1: 5.times.each { |n| puts "ciclo numero: #{n}" } 2: #=> ciclo numero: 0 3: #=> ciclo numero: 1 4: #=> ciclo numero: 2 5: #=> ciclo numero: 3 6: #=> ciclo numero: 4
5.times
è un metodo dei numeri che di fatto genera un range da 0..4
: partendo da zero per cinque volte.
La tendenza quindi è ad in qualche modo trasformare un valore od usare una collezione per questo scopo.
each
Il metodo è un ciclo imperativo, quindi il suo valore restituito è ignorabile pur avendolo: restituisce semplicemente l'oggetto su cui è invocato. Altri linguaggi lo hanno magari come forEach
per esempio.
Anche gli Hash
sono degli enumerabili e quindi è applicabile anche ad essi. Il blocco passato prenderà invece che un argomento, due: chiave e valore.
1: h = {pane: 2, carne: 10, frutta: 5} 2: h.each {|k, v| puts "il prezzo di #{k} è #{v}" } 3: #=> il prezzo di pane è 2 4: #=> il prezzo di carne è 10 5: #=> il prezzo di frutta è 5
Applicando un blocco con un solo argomento, il valore passato sarà una coppia:
1: h.each {|k| puts "chiave, valore: #{k}" } 2: #=> chiave, valore: [:pane, 2] 3: #=> ...
Nel caso ci servisse l'indice corrente each_with_index {|el, index| }
, se volessimo operare qualche trasformazione:
1: %w(pane latte burro).each_with_object("") { |e, s| s << e + ','} 2: #=> "pane,latte,burro,"
Quello che fa join
praticamente ma peggio :
1: %w(pane latte burro).join(',') 2: #=> "pane,latte,burro"
map
Due delle operazioni fondamentali della programmazione funzionale sono: operare su una collezione restituendo un'altra collezione (map) e trasformare una collezione in qualcosa d'altro (reduce).
La map
è simile ad each
a parte il fatto che restituisce una nuova collezione.
1: (1..5).map { |n| n + 1 } 2: #=> [2, 3, 4, 5, 6] 3: 4: %w(roma genova perugia livorno).map {|c| c.upcase } 5: #=> ["ROMA", "GENOVA", "PERUGIA", "LIVORNO"] 6: 7: %w(roma genova perugia livorno).map {|c| c.capitalize } 8: #=> ["Roma", "Genova", "Perugia", "Livorno"]
map
funziona anche per gli Hash
anche se restituisce un array:
1: {pane: 2, carne: 10, frutta: 5}.map {|k, v| "#{k}: #{v}"} 2: #=> ["pane: 2", "carne: 10", "frutta: 5"]
reduce
(in altri linguaggi fold
)
L'operazione di reduce
è leggermente più articolata, ma non per questo complessa. La funzione oltre al blocco da applicare accetterà un valore iniziale che ne determinerà anche il tipo del valore in uscita. Il blocco dovrà avere due argomenti: un accumulatore e l'oggetto corrente dell'iterazione.
Sommiamo tutti i valori di una lista:
1: (1..5).reduce(0) {|memo, v| memo += v} 2: => 15
Troviamo quanti alimenti per tipo:
1: TIPI_ALIMENTO = { 2: "pere": "frutta", 3: "mele": "frutta", 4: "succo": "bibita", 5: "manzo": "carne", 6: "sarago": "pesce", 7: "mozzarella": "latticino" 8: } 9: 10: def trova_per_tipo(t) 11: TIPI_ALIMENTO.reduce([]) {|memo, c| memo << c[0].to_s if c[1] == t; memo} 12: end 13: 14: p trova_per_tipo("frutta") 15: p trova_per_tipo("pesce")
reduce
ed inject
sono degli alias
e non ci sono penalità nell'usare l'uno o l'altro come i tutti gli alias
.
reduce
ha altre forme:
1: (1..5).reduce(:+) 2: #=> 15 3: 4: (1..5).reduce(10, :+) 5: #=> 25
Se non è specificato il valore iniziale come argomento verrà preso il primo elemento della collezione. Il simbolo (in questo caso :+
) è quello riferito al un nome di metodo in grado di essere invocato per la classe dell'elemento.
Vediamo come potremmo cambiare il nostro filtro per il cibo:
1: TIPI_ALIMENTO = { 2: "pere": "frutta", 3: "mele": "frutta", 4: "succo": "bibita", 5: "manzo": "carne", 6: "sarago": "pesce", 7: "mozzarella": "latticino" 8: } 9: 10: class AccArray < Array 11: def initialize(t) 12: @t = t 13: end 14: 15: def get(tuple) 16: self << tuple[0].to_s if tuple[1] == @t 17: self 18: end 19: end 20: 21: def trova_per_tipo(t) 22: TIPI_ALIMENTO.reduce(AccArray.new(t), :get) 23: end 24: 25: p trova_per_tipo("frutta") 26: p trova_per_tipo("pesce")
Usando una lambda
si sarebbe potuto scrivere:
1: # ... 2: def trova_per_tipo(t) 3: get = ->(memo, c) {memo << c[0].to_s if c[1] == t; memo} 4: TIPI_ALIMENTO.reduce([], &get) 5: end 6: # ...
flat_map
Alle volte si ha l'esigenza di concatenare i risultati dell'applicazione di un blocco ad una collezione. flat_map
fa esattamente questo e potrebbe per esempio essere usata per spianare un array di array.
1: [1, [2, 3], [4,5,6], 7, 8, 9].flat_map { |e| e } 2: #=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
Anche se flatten
lavora su più livelli (su un solo livello forse flat_map
è più veloce):
1: [1, [2, 3], [4,5,6], [[7,8]]].flatten 2: #=> [1, 2, 3, 4, 5, 6, 7, 8]
Inserire qualcosa tra gli elementi:
1: %w(Paola Marco).flat_map {|e| [e, "ciao!"]} 2: #=> ["Paola", "ciao!", "Marco", "ciao!"] 3: %w(Paola Marco).flat_map {|e| "#{e} ciao!"} 4: #=> ["Paola ciao!", "Marco ciao!"]
zip
ovvero come mischiare il tutto
zip
si fa prima a vedere che a spiegare:
1: %w(Paola Giorgio Andrea) 2: #=> ["Paola", "Giorgio", "Andrea"] 3: %w(buongiorno buonasera buonanotte) 4: #=> ["buongiorno", "buonasera", "buonanotte"] 5: nomi.zip(saluti) 6: #=> [["Paola", "buongiorno"], ["Giorgio", "buonasera"], ["Andrea", "buonanotte"]] 7: titoli = %w(Dott. Sig. Caro) 8: #=> ["Dott.", "Sig.", "Caro"] 9: titoli.zip(nomi, saluti) 10: #=> [["Dott.", "Paola", "buongiorno"], ["Sig.", "Giorgio", "buonasera"], ["Caro", "Andrea", "buonanotte"]]
1: nomi = %w(Paola Giorgio Andrea) 2: saluti = %w(buongiorno buonasera buonanotte) 3: titoli = %w(Dott. Sig. Caro) 4: 5: titoli.zip(nomi, saluti).map { |e| [e.take(2), "#{e.last}!"].flatten.join(' ') } 6: #=> ["Dott. Paola buongiorno!", "Sig. Giorgio buonasera!", "Caro Andrea buonanotte!"]
1: nomi = %w(Paola Giorgio Andrea) 2: saluti = %w(buongiorno buonasera buonanotte) 3: titoli = %w(Dott. Sig. Caro) 4: lettera = <<~HD 5: Le scrivo mio malgrado per comunicarle che... 6: 7: Distinti saluti 8: 9: Dr. Mario Rossi 10: HD 11: 12: lettere = titoli.zip(nomi, saluti).map { |e| 13: [[e.take(2), "#{e.last}."].flatten.join(' '), lettera] 14: } 15: 16: lettere.each_with_index { |l, i| 17: name = "lettera-#{i}.txt" 18: puts "salvo il file #{name}" 19: File.open(name, 'w') { |f| f << l.join("\n\n") } 20: }
La ricorsione a coda è una tecnica tipica nei linguaggio funzionali ed in breve consiste nel recuperare la chiamata di funzione non allocando lo stack delle chiamate. In Ruby la questione è particolare e non la supporta in maniera predefinita ma può essere attivata a livello di compilazione o a runtime; inoltre non è sempre garantita.
// vm_opts.h //... /* Compile options. * You can change these options at runtime by VM::CompileOption. * Following definitions are default values. */ #define OPT_TAILCALL_OPTIMIZATION 0 #define OPT_PEEPHOLE_OPTIMIZATION 1 #define OPT_SPECIALISED_INSTRUCTION 1 #define OPT_INLINE_CONST_CACHE 1 #define OPT_FROZEN_STRING_LITERAL 0 #define OPT_DEBUG_FROZEN_STRING_LITERAL 0 // ...
Facciamo qualche benchmark su due implementazioni di una funzione fib(n)
, la classica funzione di Fibonacci dove i primi due numeri sono 1 mentre quelli che seguono la somma dei due precedenti.
1: require 'benchmark' 2: include Benchmark 3: 4: high_limit = 39 5: 6: def fib(x) 7: x < 2 ? 1 : fib(x - 1) + fib(x - 2) 8: end 9: 10: def fib_range(r) 11: r.map { |n| fib(n) } 12: end 13: 14: Benchmark.benchmark(CAPTION, 7, FORMAT, ">total:", ">avg:") { |x| 15: rep = x.report(:fib_range){ fib_range(1..high_limit) } 16: [rep, rep] 17: }
fib_range 20.439008 0.000000 20.439008 ( 20.438642)
Cambiamo la nostra implementazione usando una ricorsione in coda
1: require 'benchmark' 2: include Benchmark 3: 4: high_limit = 39 5: 6: def fib(x) 7: def fun(x, nxt, cur) 8: x == 0 ? cur : fun(x - 1, cur + nxt, nxt) 9: end 10: fun(x, 1, 0) 11: end 12: 13: def fib_range(r) 14: r.map { |n| fib(n) } 15: end 16: 17: Benchmark.benchmark(CAPTION, 7, FORMAT, ">total:", ">avg:") { |x| 18: rep = x.report(:fib_range){ fib_range(1..high_limit) } 19: [rep, rep] 20: }
fib_range 0.000069 0.000000 0.000069 ( 0.000065)
Attiviamo l'ottimizzazione ma il codice deve essere ricompilato a runtime in questo caso.
1: require 'benchmark' 2: include Benchmark 3: 4: high_limit = 39 #10100 5: 6: def fib_range_recomp(r) 7: RubyVM::InstructionSequence.compile_option = { 8: tailcall_optimization: true, 9: trace_instruction: false 10: } 11: RubyVM::InstructionSequence.new(<<-EOS).eval 12: def fib(x) 13: def fun(x, nxt, cur) 14: x == 0 ? cur : fun(x - 1, cur + nxt, nxt) 15: end 16: fun(x, 1, 0) 17: end 18: def fib_range(r) 19: r.map { |n| fib(n) } 20: end 21: fib_range(#{r}) 22: EOS 23: end 24: 25: Benchmark.benchmark(CAPTION, 7, FORMAT, "> total:", "> avg:") { |x| 26: rep = x.report(:fib_range_recomp){ fib_range_recomp(0..high_limit) } 27: [rep, rep] 28: }
fib_range_recomp 0.000155 0.000009 0.000164 ( 0.000159)
Questa versione è ovviamente più lenta della sua controparte non ricompilata ma lo scopo non era la velocità quanto il superamento del limite della ricorsività.
Se tentate con iterazioni oltre i 10000 e qualcosa le prime due falliranno con uno SystemStackError
.
Questa è una tecnica spesso usata nella programmazione funzionale ma non solo (non è specifica) e permette di immagazzinare i risultati di una computazione per non ricalcolarli ogni volta. Quindi se ci aspettiamo dei risultati coerenti e ripetitivi potrebbe essere utile.
1: module Memoization 2: def memoize(mname) 3: @@lt ||= Hash.new { |h, k| h[k] = {} } 4: fun = Module.instance_method(mname) 5: define_method(mname) { |*args| 6: @@lt[mname][args] = fun.bind(self).call(*args) unless @@lt[mname].include?(args) 7: @@lt[mname][args] 8: } 9: end 10: end
Il modulo Memoization
ha un solo metodo memoize
che si dovrà occupare di creare un Hash
come variabile di classe.
L'hash avrà una struttura di questo tipo: {:fib=>{[0]=>0, [1]=>1, ...}
una volta costruito. Il metodo Module.instance_method
è in grado di recuperare il metodo come #<UnboundMethod: Module(Object)#nome_della_funzione>
ed è per questo che l'invocazione dovrà collegarlo alla classe corrente (self).
Ora che abbiamo un modulo Memoization usiamolo con le funzioni per la sequenza di Fibonacci di cui sopra.
1: require 'benchmark' 2: include Benchmark 3: 4: require './memoization' 5: include Memoization 6: 7: high_limit = 20000 8: 9: memoize( 10: def fib(x) 11: x < 2 ? x : fib(x - 1) + fib(x - 2) 12: end 13: ) 14: 15: def fib_range(r) 16: r.map { |n| fib(n) } 17: end 18: 19: Benchmark.benchmark(CAPTION, 7, FORMAT, ">total:", ">avg:") do |x| 20: rep = x.report(:fib_range){ fib_range(0..high_limit) } 21: [rep, rep] 22: end
La versione precedente oltretutto non avrebbe retto 20000 iterazioni.
La natura dinamica ed altamente riflessiva rendono Ruby molto adatto alla metaprogrammazione ed alcuni esempi sono stati fatti in precedenza. Per questo supporto ci sono numerosi metodi che permettono la manipolazione a runtime degli oggetti istanziati.
define_method
Il metodo permette di definire all'interno della classe nuovi metodi. Va ricordato che le classi sono aperte e che in ogni momento si può aggiungere qualcosa. Con define_method
lo possiamo fare in due modi visto che è un metodo di classe (appartiene al modulo Module
come la maggior parte di questi metodi).
1: PERSON = { 2: name: "Mario", 3: lastname: "Rossi" 4: } 5: 6: class HashAccessor 7: 8: def initialize(data) 9: @data = data 10: end 11: 12: define_method(:name) { @data[:name] } 13: define_method(:"name=") { |v| @data[:name] = v } 14: define_method(:lastname) { @data[:lastname] } 15: define_method(:"lastname=") { |v| @data[:lastname] = v } 16: 17: end 18: 19: person = HashAccessor.new(PERSON) 20: 21: puts person.name 22: person.name = "Maria" 23: puts person.name 24: puts person.lastname
Questa forma è poco pratica, se volessimo aggiungere una coppia key-value a DATA
?
Così invece non abbiamo problemi. Ogni instanza ha sempre la possibilità di rintracciare la classe a cui appartiene con self.class
ed il metodo send
manderà un messaggio a quell'oggetto (la classe è essa stessa una istanza della classe Class
).
1: PERSON = { 2: name: "Mario", 3: lastname: "Rossi" 4: } 5: 6: class HashAccessor 7: 8: def initialize(data) 9: @data = data 10: prepareClass 11: end 12: 13: def prepareClass 14: @data.each { |key, value| 15: self.class.send(:define_method, key.to_s) { @data[key] } 16: self.class.send(:define_method, "#{key.to_s}=") { |v| @data[key] = v } 17: } 18: end 19: 20: end 21: 22: person = HashAccessor.new(PERSON) 23: 24: puts person.name 25: person.name = "Maria" 26: puts person.name
Ruby ha mutuato il gergo da Smalltalk dove il metodo è un messaggio verso la classe in grado di comprenderlo. Quindi l'invocazione di un metodo è inevitabilmente l'invio di un messaggio.
1: d = HashAccessor.new({key: "value"}) 2: #=> #<HashAccessor:0x00007f5a4827e030 @data={:key=>"value"}> 3: d.key 4: #=> "value" 5: d.send(:key) 6: #=> "value" 7: d.send(:"key=", "pippo") 8: #=> "pippo" 9: d.key 10: #=> "pippo"
Possiamo giocare con le lettere dell'alfabeto:
1: class Alfabeto 2: def initialize 3: @text = [] 4: end 5: 6: def text 7: @text.join 8: end 9: 10: #https://unicode-table.com/en/search/?q=heart 11: # 💕 = U+1F495 12: (('a'..'z').to_a << '_' << '💕').each { |l| 13: define_method(l.to_sym) { 14: @text << (l == '_' ? " " : l) 15: self 16: } 17: } 18: end 19: 20: alf = Alfabeto.new 21: p alf.m.a.r.i.o._.r.o.s.s.i._.💕.text
Con instance_methods
, methods
, private_instance_methods
, private_methods
, protected_instance_methods
, protected_methods
, public_instance_methods
, public_methods
, singleton_methods
si può avere una lista delle varie tipologie di metodi di classi ed istanze. Più o meno analoghe funzioni esistono per le variabili.
Come si può aggiungere si può togliere: method_removed
, remove_class_variable
, remove_const
, remove_instance_variable (Object)
, remove_method (Module)
, singleton_method_removed
.
Per consultare la documentazione cercare soprattutto in Object
, Module
, BasicObject
, Class
.
Ruby è fornito con le batterie incluse. Ha una estesa libreria di base consultabile online a questo url: http://ruby-doc.org/core; oltre ciò una libreria standard di funzionalità anche questa consultabile online: http://ruby-doc.org/stdlib/.
Con la distribuzione sono presenti due comandi per gestire la documentazione: ri
per consultare la documentazione allegata ed rdoc
per generare nuova documentazione estraendo dai sorgenti i commenti.
➜ ri Array = Array < Object ------------------------------------------------------------------------ = Includes: Enumerable (from ruby core) (from ruby core) ------------------------------------------------------------------------ Arrays are ordered, integer-indexed collections of any object. ...
rdoc
è in grado di generare documentazione in vari formati da HTML al formato usato da ri
con il parametro -r
o --ri=
.
RDoc può usare vari sistemi di formattazione dal proprio (RDoc::Markup) come predefinito a Markdown, TomDoc e RD.
1: ## 2: # La classe *Pippo* rappresenta un elemento bla, bla 3: 4: class Pippo 5: ## 6: # inizializza un oggetto di classe Pippo dato l'argomento +value+ 7: # 8: # Farà tante cose interessanti 9: # * prima cosa 10: # * seconda cosa 11: # * terza cosa 12: 13: def inizialize(value) 14: # ... 15: end 16: 17: end
La documentazione: https://ruby.github.io/rdoc/
Ruby possiede dei sofisticati gestori di librerie sia globali che locali al progetto.
Ruby possiede un gestore di librerie (chiamate gemme) il cui comando è gem
. Una gemma si installa nel sistema con il comando: gem install <nome della gemma>
.
Nella fase di installazione viene anche generata la documentazione tramite rdoc
, generazione che si può inibire coi parametri --no-ri
e --no-rdoc
.
RubyGems installa le gemme in un percorso predefinito gestibile con delle variabili di ambiente e fornisce all'interprete le giuste direttive per poterle trovare e richiedere (require
) durante l'esecuzione.
La lista delle gemme installate può essere consultata con gem list
mentre quelle disponibili online da poter installare con gem list --remote
.
La disinstallazione avverrà col comando gem uninstall <gemma>
che in caso di più versioni installate presenterà una scelta.
Sito web: https://rubygems.org/
Bundler è un sofisticato gestore di gemme per i progetti. È esso stesso una gemma e va installato (ancora) con gem install bundler
.
All'interno della cartella del progetto si creerà un file di testo chiamato Gemfile con bundle init
e si aggiungeranno le direttive sulle librerie e versioni da installare.
1: source 'https://rubygems.org' 2: ruby '2.5.0' 3: gem 'thin' 4: gem 'oj' 5: gem 'rake' 6: gem 'activesupport', '>= 3.1' 7: gem 'bcrypt' 8: gem 'sass' 9: gem 'slim' 10: gem 'sqlite3' 11: gem 'sequel' 12: gem 'carrierwave' 13: gem 'carrierwave-sequel', :require => 'carrierwave/sequel' 14: gem 'mini_magick', :require => 'mini_magick' 15: gem 'kramdown' 16: gem 'paypal-sdk-rest' 17: gem 'padrino', '0.14.2'
Il comando bundle install
provvederà allo scaricamento dalla rete ed alla loro installazione, generando nel contempo il file Gemfile.lock dove saranno salvati tutti i nomi e versioni comprese le dipendenze delle gemme richieste.
L'applicazione dovrà poi essere eseguita tramite bundle exec [--keep-file-descriptors] comando
che provvederà ad impostare l'ambiente giusto per l'esecuzione.
Sito web: https://bundler.io/
In Ruby sono stati scritti alcuni dei web framework più usati nello sviluppo di applicazioni web, un nome su tutti Ruby On Rails (https://rubyonrails.org/). Rails in abbreviazione, è un full stack framework ovvero un insieme di librerie e di metodologie che comprende l'intero aspetto dello sviluppo web.
Nel tempo, per alcuni, Rails è diventato un ambiente troppo stringente e rigido e quindi altri framework più agili sono apparsi.
In risposta a Rails apparve Merb che non era altro che un Rails smontato e reso più modulare. Merb rientrò in qualche modo in quello che poi fu Rails 3.
Con Camping di _why (https://en.wikipedia.org/wiki/Why_the_lucky_stiff una delle figure più interessanti nella comunità Ruby) iniziò la via dei microframework, più flessibili ed adattabili alle varie esigenze. Sinatra (http://sinatrarb.com che è basato su Rack https://rack.github.io/) è diventato di fatto il microframework di elezione, tanto da vantare imitazioni in molti altri linguaggi di programmazione.
Una applicazione Sinatra è molto semplice dopo aver installato la gemma con gem install sinatra
.
1: require 'sinatra' 2: 3: get '/' do 4: 'Hello world!' 5: end
get '/' do-end
è una rotta HTTP di tipo GET
. In Ruby se non c'è ambiguità le parentesi possono essere omesse e quindi equivale a:
1: require 'sinatra' 2: 3: get('/') { 4: 'Hello world!' 5: }
Una rotta è un metodo (get
, post
, put
, patch
, delete
, options
, link
, unlink
) che ha come argomento minimo il percorso che si vuole esporre ed un blocco che elaborerà la risposta.
Il percorso della rotta può essere descritto in vari modi per esempio inserendo una variabile.
1: require 'sinatra' 2: 3: get '/hello/:who' do 4: "Hello #{params[:who]}" 5: end
Una chiamata a http://localhost:4567/hello/mario scriverà Hello mario. L'Hash
params
è disponibile all'interno del blocco per catturare le parti o le query indicate nell'URL.
Un altro modo di catturare i parametri fornendo argomenti al blocco.
1: require 'sinatra' 2: 3: get '/hello/:who' do |who| 4: "Hello #{who}" 5: end
Lo splat è disponibile per fornire parti anonime dell'URL.
1: require 'sinatra' 2: 3: get '/*/*/ciao' do 4: "ciao #{params[:splat].join(" ")}, benvenuto!" 5: end
Si possono avere diversi tipi di rotte: espressioni regolari, con parametri opzionali, query parameters …
I metodi HTTP in Sinatra possono accettare ulteriori argomenti per stabilire quale metodo invocare.
1: get '/solo_firefox', :provides => ['html'] do 2: "<html><head></head><body>hello</body></html> 3: end
Un test banale sullo UserAgent.
1: require 'sinatra' 2: 3: get "/", :agent => /.*Vivaldi.*/ do 4: p request.user_agent 5: "è Vivaldi" 6: end 7: 8: get "/" do 9: "non è Vivaldi" 10: end
Si possono creare delle condizioni personalizzate.
1: require 'sinatra' 2: 3: def current_user 4: {name: "Mario", lastname: "Rossi", role: :any} 5: end 6: 7: set(:auth) do |*roles| 8: condition do 9: if roles.include?(current_user[:role]) 10: true 11: else 12: redirect "login", 303 13: end 14: end 15: end 16: 17: get "/admin", :auth => [:admin] do 18: "Amministrazione" 19: end 20: 21: get "/area", :auth => [:any] do 22: "Area pubblica" 23: end 24: 25: get "/login" do 26: "LOGIN" 27: end
Il framework dispone di alcune impostazioni predefinite e personalizzabili e se ne possono creare di nuove da usare nell'applicazione.
1: require 'sinatra' 2: 3: set :riferimento, valore 4: set :riferimento_ritardato, ->() { "qualcosa da fare } 5: 6: enable :logging #abilita un'impostazione booleana 7: disable: dump_errors #disabilita un'impostazione booleana 8: 9: set :root, File.dirname(__FILE__) #la radice dell'applicazione 10: enable :static #Se il sistema userà o non userà la public per i file statici 11: set :public_folder, ->(){ File.join(root, "static") } #Imposta la directory dove risiederanno i file statici. 12: set :views, ->(){ File.join(root, "views") } #Imposta la directory dove risiederanno i template dei vari generatori di markup. 13: 14: set :server, %w[puma, thin, webrick] #la lista dei server Rack compliant da utilizzare nell'ordine. 15: set :port, 3000 #la porta di ascolto del server 16: set :bind, '0.0.0.0' #L'interfaccia dove il server ascolterà 17: 18: get "/" do 19: "Hello" 20: end
Il framework ha il supporto per numerosi sistemi di templating e relativa applicazione di layout.
Tra i principali:
Haml (http://haml.info/), Erb (incluso in Ruby), Liquid (https://shopify.github.io/liquid/), Markdown (https://kramdown.gettalong.org/), SCSS (http://sass-lang.com/), Less (http://lesscss.org/). Vedere la documentazione per la lista completa dei tipi e delle dipendenze (http://sinatrarb.com/intro.html).
Ognuno dei sistemi ha associato un metodo che deve essere invocato per la trasformazione dal modello al contenuto servito.
Per Slim per esempio, slim :index
caricherà dalla directory views (se non impostata diversamente) un file chiamato index.slim
.
Si presume che Bundler sia installato, altrimenti: gem install bundler
.
Creeremo una struttura di cartelle in questo modo:
├── helpers ├── public │ └── images └── views ├── layouts └── scss
Per prima cosa sulla radice scriveremo il Gemfile per l'impostazione delle gemme di progetto
1: source "https://rubygems.org" 2: 3: gem 'thin' 4: gem 'sass' 5: gem 'slim' 6: gem 'sinatra'
Con bundle install
si installeranno nel sistema e verrà generato il file Gemfile.lock. Si userà come web server Thin, come sistema di template Slim e come linguaggio per i CSS SASS.
Per provare se il sistema troverà il file HTML statico ne faremo uno nella cartella public col nome di test.html.
1: <html> 2: <head> 3: <title>Titolo</title> 4: </head> 5: <body> 6: Pagina statica 7: <p> 8: <a href="/">HOME</a> 9: </p> 10: </body> 11: </html>
Inseriremo anche una immagine dal nome image.png nella cartella public/images e la nostra parte statica è pronta.
Ci serviranno alcuni metodi di aiuto (helpers) da usare all'interno del template quindi scriveremo un file all'interno della cartella helpers che chiameremo main_helpers.rb.
1: module MainHelpers 2: 3: def link_to(text, href, **options) 4: %(<a href=#{href} title=#{options[:title].to_s} class=#{options[:class]}>#{text}</a>) 5: end 6: 7: def image(name, **options) 8: %(<img src="/images/#{name}" alt="#{options[:alt].to_s} />) 9: end 10: end
Sono due classici metodi helper ritrovabili anche in Rails per esempio.
Passiamo alla applicazione vera e propria.
1: require 'sinatra' 2: require './helpers/main_helpers' 3: helpers MainHelpers 4: 5: set :server, %w[thin] 6: set :port, 3000 7: set :bind, '0.0.0.0' 8: 9: get "/" do 10: slim :index, :layout => :"layouts/layout", :locals => {year: 2018, author: "Braccobaldo"} 11: end 12: 13: get "/stylesheets/style.css", :provides => :css do 14: scss :"scss/style" 15: end
L'applicazione è molto semplice, si è richiesto Sinatra, il file degli helpers e si è incluso il modulo con una direttiva Sinatra fornita dal framework. Si è impostato il web server, la sua porta e l'indirizzo di ascolto.
Alla rotta root si è assegnato un layout (che vedremo dopo) ed un Hash
che contiene delle variabili locali al template che poi verrà renderizzato. Slim supporta questo meccanismo, altri sistemi come Markdown no.
Prepariamo il primo template in Slim dentro la cartella views chiamato index.slim.
1: h1 Pagina Web 2: p Sono il testo della pagina web 3: 4: p==link_to("pagina statica", "/test.html") 5: 6: p.image== image("image.png")
Slim è una sorta di HTML ridotto senza parentesi angolari e si rimanda alla documentazione per approfondire (http://slim-lang.com/).
Come si vede si sono usati i metodi helper per generare i tag dell'ancora e dell'immagine.
Ci manca il layout.
1: doctype html 2: html 3: head 4: title Web Page 5: link rel="stylesheet" href="/stylesheets/style.css" type="text/css" media="all" 6: body 7: div#header 8: div#content 9: == yield 10: div#footer Copyright © #{year} #{author}
Questo scheletro verrà riempito con il template nel punto in cui si trova yield
renderizzando il tutto e servendolo al browser.
Per ultimo abbiamo il file dello stile CSS.
1: body { 2: font-size: 16px; 3: font-family: "Arial"; 4: background-color: #cccccc; 5: 6: a { 7: color: #ffffff; 8: font-weight: bold; 9: background-color: #0076ff; 10: padding: 0.6em; 11: border: 1px solid #0226ff; 12: 13: &:hover { 14: background-color: #0226ff; 15: } 16: } 17: }
Possiamo poi fare un file di shell, renderlo eseguibile e lanciarlo.
1: #!/bin/bash 2: 3: #giusto per essere sicuri che ci siano le gemme 4: bundle install 5: 6: bundle exec ruby app.rb
o semplicemente lanciare la linea di comando indicata.
La struttura finale del progetto.
├── helpers │ └── main_helpers.rb ├── public │ ├── images │ │ └── image.png │ └── test.html ├── views │ ├── layouts │ │ └── layout.slim │ ├── scss │ │ └── style.scss │ └── index.slim ├── app.rb ├── Gemfile ├── Gemfile.lock └── start.sh
Penso sempre molto a come concludere un discorso su Ruby ma poi alla fine non mi viene in mente altro che: