Appunti di programmazione in Ruby

Massimo Maria Ghisalberti - pragmas.org

01/11/2018

Created: 2018-12-06 gio 13:19

Versione in pdf

Questo documento è disponibile in formato pdf.
Questa presentazione ed il file pdf sono prodotti da un unico sorgente in org-mode.

Ruby

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.

Alcune caratteristiche.

  • Sintassi semplificata e flessibile
  • Linguaggio ad oggetti puro con ereditarietà singola, mixin e metaclassi
  • i costrutti sono espressioni
  • Tipizzazione dinamica, duck typing
  • Riflessione dinamica e metaprogrammazione
  • Closure, iteratori e generatori
  • Argomenti predefiniti nei metodi
  • Interpolazione delle stringhe
  • Sintassi letterale per le strutture dati
  • Thread e fiber
  • Gestione delle eccezioni
  • REPL
  • API per estensioni in C
  • Gestione centralizzata delle librerie (RubyGems)
  • multipiattaforma
  • Batterie incluse
  • estensione del file sorgente: rb

Sintassi e semantica.

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à:

  • globale, prefisse con $: $variabile
  • classe, prefisse con @@: @@variabile
  • istanza, prefisse con @: @variabile
  • locale, senza prefisso: 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:

  • attr_accessor: getter e setter
  • attr_reader: getter
  • attr_writer: setter

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)

sorgente

Ruby è anche un linguaggio funzionale seppure impuro considerando che:

  • ha funzioni anonime sotto forma di lambda e Proc, le closure normalmente dette blocks
  • le funzioni sono quindi oggetti di prima classe (first-class)
  • le istruzioni sono a tutti gli effetti delle espressioni ed hanno un valore
  • le funzioni restituiscono l'ultimo valore della loro computazione
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

sorgente

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

sorgente

Interprete e REPL

Esistono diverse implementazioni del linguaggio Ruby:

Installazione

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/

Interprete (Ruby MRI)

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}"

sorgente

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(',')}"

sorgente

Per consultare la lista completa: https://ruby-doc.org/core-2.5.3/doc/globals_rdoc.html

Interactive Ruby Shell

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:code14: 

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:

  • sintassi colorata
  • una architettura a plugin
  • funzioni di documentazione
  • navigazione nei moduli con ls, cd e altri comandi
  • integrazione con vari editor
1: 
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: 

Elementi base del linguaggio

Parole chiave

__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

Letterali

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

nil e Booleani

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

Numeri

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.

Stringhe

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: 

Stringhe lunghe (HEREDOC)

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

sorgente

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

sorgente

Simboli

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

sorgente

Array

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]

Espressioniregolari (Regexp)

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

Hash (Dizionari)

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).

Range (intervalli)

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 -1
  • a <=> b, se a = b allora 0
  • a <=> b, se a > b allora 1
  • se a e b non sono comparabili allora nil

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

sorgente

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

sorgente

Proc e lambda

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

sorgente

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

Costanti e 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.

Strutture di controllo e cicli

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: }

sorgente

Il parametro &block è la proc che passeremo alla funzione e che sarà invocata nel ciclo sulla collezione.

Metodi

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: }

sorgente

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]

Eccezioni

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

sorgente

Organizzazione per file.

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')

Programmazione orientata agli oggetti

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.

Classi e Moduli

 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")

sorgente

I moduli possono essere annidati.

 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")

sorgente

L'operatore di visibilità ::

 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

sorgente

Anche le classi possono essere annidate.

 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: 

sorgente

Ereditarietà

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

sorgente

Visibilità

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

sorgente

Metodi di classe e metaclassi

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)

sorgente

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)

sorgente

Classi aperte

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.

Programmazione funzionale

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")

sorgente

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")

sorgente

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!"]

Prepariamo qualche lettera…

 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: }

sorgente

Tail recursion

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: }

sorgente

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: }

sorgente

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: }

sorgente

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.

Memoization

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

sorgente

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

sorgente

La versione precedente oltretutto non avrebbe retto 20000 iterazioni.

Metaprogrammazione

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

sorgente

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

sorgente

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

sorgente

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.

Batterie incluse e documentazione

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/

RubyGems - Bundler

Ruby possiede dei sofisticati gestori di librerie sia globali che locali al progetto.

RubyGems

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

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/

Web Framework

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.

Sinatra

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

sorgente

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: }

sorgente

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

sorgente

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

sorgente

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

sorgente

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

sorgente

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

sorgente

Le impostazioni di Sinatra

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

I template in Sinatra

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.

Un piccolo progetto

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'

sorgente

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>

sorgente

Inseriremo anche una immagine dal nome image.png nella cartella public/images e la nostra parte statica è pronta.

image.png

sorgente

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

sorgente

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

sorgente

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")

sorgente

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}

sorgente

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: }

sorgente

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

Conclusioni

Penso sempre molto a come concludere un discorso su Ruby ma poi alla fine non mi viene in mente altro che:

  • Ruby è divertente.
Licenza Creative Commons Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Non commerciale - Non opere derivate 4.0 Internazionale.