Chiusure rubino spiegato.

Chiusure rubino spiegato. Foto di Denise Johnson

A meno che tu non abbia capito il concetto di “chiusura” da altre lingue, potrebbe esserti stato fonte di confusione inizialmente in Ruby. “Closure”, è anacronistico, tranne che per Ruby (e alcuni linguaggi di programmazione funzionali).

Ecco una definizione Merriam-Webster di una chiusura:

chiusura sostantivo clo * sicuro \ ˈklō-zhər \ arcaico: mezzi per racchiudere: enclosure

È lo stesso qui: Una chiusura Ruby “racchiude un mucchio di codice da eseguire”. È un “recinto”. Secondo Wikipedia, una chiusura è una tecnica per implementare l’associazione di nomi con ambito lessicale in una lingua con funzioni di prima classe.

Considera questo frammento:

2.times do topic = 'Ruby closures' puts "An adventure with #{topic}."end#=> An adventure with Ruby closures.#=> An adventure with Ruby closures.#=> 2

Abbiamo “racchiuso” topic e puts tra doe end. Qualsiasi cosa al di fuori di quella regione non può avere accesso a topic, ma il metodo puts può perché si trova all’interno dello stesso ambiente (con ambito lessicale) dell’assegnazione della variabile (associazione del nome). È questa tecnica di combinazione di metodi e variabili che chiamiamo chiusura. Nota: { ... } e do ... end sono equivalenti quando passati a un metodo. Li userò in modo intercambiabile.

Che ne dici di questo pezzo di codice?

lam = lambda do |topic| puts "An adventure with #{topic}."endlam.call('Ruby closures')#=> An adventure with Ruby closures

Se guardi da vicino, sta facendo una cosa simile a quella del frammento prima. Ora, che ne dici dell’ultimo?

prok = Proc.new do |topic| puts "An adventure with #{topic}."endprok.call('Ruby closures')#=> An adventure with Ruby closures.

Queste forme rappresentano modi per racchiudere il codice in Ruby. Abbiamo visto un blocco, lambda e un Proc rappresentati rispettivamente come: {}, lambda {} e Proc.new {} nei frammenti di codice precedenti. Queste sono chiusure in Ruby. In tutti e tre, abbiamo impostato una variabile locale topic e l’abbiamo passata a un metodo locale puts. Ci si potrebbe chiedere dove abbiamo impostato la variabile topic in lame prok. Non l’abbiamo impostato esplicitamente come topic = "Ruby closures" come abbiamo fatto nel primo frammento di codice. Il motivo è; il metodo #call per Proc e lambda fa questo per noi. Se si guarda la documentazione per Proc#call, abbiamo:

call (params,→) → objinvoca il blocco, impostando i parametri del blocco sui valori in params.

Una sottile differenza che vale la pena notare sui blocchi Ruby (come chiusure) è che, in Ruby, i blocchi non possono essere rappresentati come oggetti, forniscono solo un modo semplice per raggruppare il codice ma non è possibile legarli alle variabili. Da soli, sono abbastanza inutili. Proc e Lambda, d’altra parte, sono, ciò che alcuni chiamano cittadini di prima classe.

Blocchi.

Devo essere corretto, ma come ho detto prima, i blocchi da soli non significano nulla. Di solito vengono passati come argomenti ai metodi, ma ci aiutano anche a raggruppare il nostro codice per modificarne il comportamento. Ecco un esempio:

(1..10).each { |v| puts v if v.even? }# Output: ## 2# 4# 6# 8# 10

È facile vedere qui che stiamo iterando un intervallo e stampando tutti i numeri pari. Ora, cosa succede se vogliamo stampare tutti i numeri che sono pari e primo allo stesso tempo? Ecco quando i blocchi mostrano la loro utilità. Dobbiamo solo modificare il codice nel blocco per fare questo per ottenere:

require 'prime'(1..20).each { |v| puts v if v.even? && v.prime? }# => 2

Abbiamo appena cambiato il comportamento del nostro codice raggruppando ciò che è nel nostro blocco. Abbiamo dovuto richiedere la libreria standard Prime per utilizzare il metodo prime?. Avremmo potuto anche implementare il nostro metodo per raccogliere i numeri primi, se volevamo. Abbiamo visto come possiamo usare i blocchi Ruby per raggruppare il nostro codice da enumerare. Ma in questo post, stiamo parlando di chiusure. Hai già visto come il blocco sopra funge da chiusura assegnando ciascun numero nell’intervallo da 1 a 20 alla variabile v? Se non, come questo pezzo di codice qui:

def shout(word, position) position.times { |x| puts "#{ x }: #{ word }"}endshout("SHOUTING!", 2)# Output: ## 0: SHOUTING!# 1: SHOUTING!

In questo, si sta assegnando il word variabile la stringa SHOUTING! assegnando il secondo parametro, un numero Intero, per la variabile position e con x per rappresentare ogni numero da 0 a position per il numero di volte in cui si desidera che la stringa stampata. Tieni a mente la definizione di chiusura. L’ultimo codice lo soddisfa?

Ora conosci un po ‘ i blocchi, ma negli esempi mostrati finora, stiamo solo usando i blocchi Ruby per incapsulare il comportamento e passarli ai metodi di Ruby. Possiamo anche scrivere i nostri metodi che accettano blocchi.

In Ruby, siamo benedetti con i metodi yield e block_given?. Questi metodi ci aiutano a creare i nostri metodi che accettano blocchi troppo. Ogni volta che vedi yield in Ruby, traducilo come “Ehi, fai tutto ciò che dice il blocco passato”. block_given? è piuttosto intuitivo. Questo metodo restituisce semplicemente un booleano del fatto che ci sia stato o meno un blocco passato. Ecco un semplice esempio per ottenere il concetto attraverso:

def shout yieldend# Call the #shout method with a block.shout { puts "SHOUTING!" }# => SHOUTING!

Prova il metodo sopra e chiama yield due o tre volte e guarda cosa succede. Ha senso? Se non hai passato un blocco a questo metodo quando lo hai chiamato, vedresti

nessun blocco dato (yield) (LocalJumpError)

Puoi provarlo. Ed è qui che block_given? è utile. Ecco come è possibile utilizzare il metodo block_given? :

def shout block_given? ? yield : puts("Sir, you didn't pass any block.")end# Call the #shout method without a block.shout# => Sir, you didn't pass any block.

Questo è praticamente tutto quello che c’è da fare! Ma potresti chiederti quale sia l’uso pratico di tutto questo: Inizializzazione di oggetti graziosi! Così:

module Genus class Species attr_accessor :name, :height, :weight, :ethnicity def initialize yield self if block_given? end endendhuman = Genus::Species.new do |characteristics| characteristics.name = "Emmanuel Hayford" characteristics.height = 188 # cm characteristics.weight = 104 # kg characteristics.ethnicity = "black"endp human.ethnicity# => "black"

Stiamo impostando self (che è un oggetto istanziato della classe Species) alla variabile characteristics.

Procs.

Anche se tutto in Ruby è un oggetto, costrutti come blocchi e metodi non lo sono. Tuttavia, possono possiamo trasformarli in oggetti con Procs. Quindi per lo più i Proc sono blocchi rappresentati come oggetti su cui è possibile chiamare metodi o allocare in memoria.

Rifattorizziamo il nostro primo esempio per usare un Proc.

Quindi avremo questo pezzo di codice:

2.times do topic = 'Ruby closures' puts "An adventure with #{topic}."end

cambiato per questo:

prok = Proc.new { topic = 'Ruby closures' puts "An adventure with #{topic}."}# `Proc.new` is equivalent to `proc`2.times do prok.callend# => An adventure with Ruby closures.# => An adventure with Ruby closures.

Perché proc è ora un oggetto, è possibile richiamare il call metodo per restituire il blocco.Potremmo rifattorizzare ulteriormente per renderlo più dinamico rendendo l’oggetto block o Proc un argomento:

prok = Proc.new { |topic| puts "An adventure with #{topic}."}2.times do prok.call('Ruby closures')end# => An adventure with Ruby closures.# => An adventure with Ruby closures.

Ecco qualcosa di interessante; per un oggetto Proc che accetta un singolo argomento, ad esempio, è possibile chiamarlo in un paio di modi diversi. Ruby ci permette di omettere la chiamata a call. Quindi la riga 6 del codice sopra è equivalente alla seguente:

prok.('Ruby closures') e prok === 'Ruby closures'.

Se, tuttavia, si desidera lavorare con più argomenti, è possibile passare i propri argomenti all’oggetto Proc come elementi di un array. Quindi supponiamo che questo sia il nostro codice:

prok = Proc.new { |x, y| puts x + y }

Tutti i metodi riportati di seguito per chiamare prok sono validi:

prok.call prok === prok# => 10# => 10# => 10

Tutto quanto sopra è valido per lambda s.

Lambda.

I lambda sono oggetti Proc che rispettano l’arity e trattano la parola chiave return in modo leggermente diverso.

Ecco il comportamento degno di nota su Procs e Lambda. Considera quanto segue:

cool_lambda = lambda { |first_name, nationality| puts "#{first_name} is #{nationality}."}cool_proc = proc { |first_name, nationality| puts "#{first_name} is #{nationality}."}cool_lambda.call("Matz", "Japanese")cool_proc.call("Aaron", "American")

Tutto funziona come previsto. La firma di cool_lambdaaccetta due argomenti ed è rigorosa su questo, se hai passato un argomento in più o in meno, esploderebbe con qualcosa di simile:

numero errato di argomenti (dato 3, previsto 2) (ArgumentError)

cool_proc d’altra parte farà solo ciò che il blocco specifica e ignorerà silenziosamente i parametri extra o la loro assenza.

Ecco il caso della parola chiave return con closures:

class ClosureExperiment def call_closure(closure) puts "Calling #{ check_closure_type(closure) }." closure.call puts "#{ check_closure_type(closure) } got called!" end def check_closure_type(unknown_closure) unknown_closure.lambda? ? "Lambda" : "Proc" endendclosure = ClosureExperiment.new.call_closure(lambda { return })# => Calling Lambda.# => Lambda got called!closure = ClosureExperiment.new.call_closure(proc { return })# => Calling Proc.

Da quanto sopra, si può osservare che mentre Procs scatta immediatamente vedendo return, Lambda esita fino a quando tutto nel loro contesto viene eseguito.

Un’altra differenza tra queste due chiusure è quando un Lambda viene definito con un argomento predefinito e passato un array, restituisce sempre l’argomento predefinito passato mentre Proc lo ignora a meno che non ci siano argomenti aggiuntivi, quindi l’argomento predefinito viene implementato come nel seguente:

lam = lambda { |a, b = :foo| p a, b }.call()# => , :foo]prok = proc { |a, b = :foo| p a, b }.call()# => prok = proc { |a, b = :foo| p a, b }.call()# => 

Sarebbe per come Procs e Lambda differiscono.

Per concludere, mi piace pensare ai Proc come blocchi rappresentati come oggetti mentre i Lambda sono una variante dei Proc che generano errori quando si passa il numero errato di argomenti.

Seguimi su Twitter per esplorare Ruby, JavaScript e le tecnologie web. Prendi in considerazione l’iscrizione alla mia newsletter o l’acquisto di un caffè se hai trovato utile uno dei miei contenuti.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.