Ruby Language
Blocchi e Procs e Lambdas
Ricerca…
Sintassi
- Proc.new ( blocco )
- lambda {| args | codice }
- -> (arg1, arg2) {codice}
- object.to_proc
- {| single_arg | codice }
- do | arg, (chiave, valore) | codice fine
Osservazioni
Fai attenzione alla precedenza degli operatori quando hai una linea con più metodi concatenati, come:
str = "abcdefg"
puts str.gsub(/./) do |match|
rand(2).zero? ? match.upcase : match.downcase
end
Invece di stampare qualcosa come abCDeFg
, come ci si aspetterebbe, stampa qualcosa come #<Enumerator:0x00000000af42b28>
- questo perché do ... end
ha una precedenza inferiore rispetto ai metodi, il che significa che gsub
vede solo l'argomento /./
e non l'argomento del blocco. Restituisce un enumeratore. Il blocco finisce per passare a puts
, che lo ignora e mostra solo il risultato di gsub(/./)
.
Per risolvere questo problema, gsub
racchiudere la chiamata gsub
tra parentesi o usare { ... }
.
proc
def call_the_block(&calling); calling.call; end
its_a = proc do |*args|
puts "It's a..." unless args.empty?
"beautiful day"
end
puts its_a #=> "beautiful day"
puts its_a.call #=> "beautiful day"
puts its_a[1, 2] #=> "It's a..." "beautiful day"
Abbiamo copiato il metodo call_the_block
dall'ultimo esempio. Qui, puoi vedere che un proc è fatto chiamando il metodo proc
con un blocco. Puoi anche vedere che i blocchi, come i metodi, hanno ritorni impliciti, il che significa che anche procs (e lambda) fanno. Nella definizione di its_a
, puoi vedere che i blocchi possono prendere argomenti splat e quelli normali; sono anche in grado di prendere argomenti predefiniti, ma non riesco a pensare a un modo per farlo. Infine, è possibile vedere che è possibile utilizzare più sintassi per chiamare un metodo - il metodo call
o []
operatore.
lambda
# lambda using the arrow syntax
hello_world = -> { 'Hello World!' }
hello_world[]
# 'Hello World!'
# lambda using the arrow syntax accepting 1 argument
hello_world = ->(name) { "Hello #{name}!" }
hello_world['Sven']
# "Hello Sven!"
the_thing = lambda do |magic, ohai, dere|
puts "magic! #{magic}"
puts "ohai #{dere}"
puts "#{ohai} means hello"
end
the_thing.call(1, 2, 3)
# magic! 1
# ohai 3
# 2 means hello
the_thing.call(1, 2)
# ArgumentError: wrong number of arguments (2 for 3)
the_thing[1, 2, 3, 4]
# ArgumentError: wrong number of arguments (4 for 3)
Puoi anche usare ->
per creare e .()
Per chiamare lambda
the_thing = ->(magic, ohai, dere) {
puts "magic! #{magic}"
puts "ohai #{dere}"
puts "#{ohai} means hello"
}
the_thing.(1, 2, 3)
# => magic! 1
# => ohai 3
# => 2 means hello
Qui puoi vedere che una lambda è quasi la stessa di una proc. Tuttavia, ci sono molti avvertimenti:
L'arbitrio degli argomenti di lambda sono applicati; passando il numero errato di argomenti a un lambda, si genera un
ArgumentError
. Possono ancora avere parametri predefiniti, parametri splat, ecc.return
dall'interno di un lambda ritorna dal lambda, mentre ilreturn
da un proc ritorna al di fuori del campo di applicazione:def try_proc x = Proc.new { return # Return from try_proc } x.call puts "After x.call" # this line is never reached end def try_lambda y = -> { return # return from y } y.call puts "After y.call" # this line is not skipped end try_proc # No output try_lambda # Outputs "After y.call"
Oggetti come argomenti di blocco ai metodi
Mettere una &
( &
commerciale) davanti a un argomento lo passerà come il blocco del metodo. Gli oggetti saranno convertiti in un Proc
utilizzando il metodo to_proc
.
class Greeter
def to_proc
Proc.new do |item|
puts "Hello, #{item}"
end
end
end
greet = Greeter.new
%w(world life).each(&greet)
Questo è un modello comune in Ruby e molte classi standard lo forniscono.
Ad esempio, Symbol
s implementa to_proc
inviandosi all'argomento:
# Example implementation
class Symbol
def to_proc
Proc.new do |receiver|
receiver.send self
end
end
end
Questo abilita l'utile &:symbol
idioma di &:symbol
, comunemente usato con oggetti Enumerable
:
letter_counts = %w(just some words).map(&:length) # [4, 4, 5]
blocchi
I blocchi sono blocchi di codice racchiusi tra parentesi graffe {}
(di solito per blocchi a linea singola) o do..end
(usati per blocchi a più linee).
5.times { puts "Hello world" } # recommended style for single line blocks
5.times do
print "Hello "
puts "world"
end # recommended style for multi-line blocks
5.times {
print "hello "
puts "world" } # does not throw an error but is not recommended
Nota: le parentesi hanno una precedenza più alta di do..end
cedimento
I blocchi possono essere utilizzati all'interno di metodi e funzioni utilizzando la parola yield
:
def block_caller
puts "some code"
yield
puts "other code"
end
block_caller { puts "My own block" } # the block is passed as an argument to the method.
#some code
#My own block
#other code
Attenzione però se il yield
viene chiamato senza blocco, genererà un LocalJumpError
. A tal fine, ruby fornisce un altro metodo chiamato block_given?
questo ti permette di controllare se un blocco è stato passato prima di chiamare rendimento
def block_caller
puts "some code"
if block_given?
yield
else
puts "default"
end
puts "other code"
end
block_caller
# some code
# default
# other code
block_caller { puts "not defaulted"}
# some code
# not defaulted
# other code
yield
può offrire argomenti anche al blocco
def yield_n(n)
p = yield n if block_given?
p || n
end
yield_n(12) {|n| n + 7 }
#=> 19
yield_n(4)
#=> 4
Mentre questo è un semplice esempio, la yield
può essere molto utile per consentire l'accesso diretto a variabili di istanza o valutazioni nel contesto di un altro oggetto. Per esempio:
class Application
def configuration
@configuration ||= Configuration.new
block_given? ? yield(@configuration) : @configuration
end
end
class Configuration; end
app = Application.new
app.configuration do |config|
puts config.class.name
end
# Configuration
#=> nil
app.configuration
#=> #<Configuration:0x2bf1d30>
Come puoi vedere usando yield
in questo modo, il codice app.configuration.#method_name
più leggibile rispetto a chiamare continuamente app.configuration.#method_name
. Invece è possibile eseguire tutte le configurazioni all'interno del blocco mantenendo il codice contenuto.
variabili
Le variabili per i blocchi sono locali al blocco (simili alle variabili delle funzioni), muoiono quando il blocco viene eseguito.
my_variable = 8
3.times do |x|
my_variable = x
puts my_variable
end
puts my_variable
#=> 0
# 1
# 2
# 8
I blocchi non possono essere salvati, muoiono una volta eseguiti. Per salvare i blocchi devi usare procs
e lambdas
.
Conversione in Proc
Gli oggetti che rispondono a to_proc
possono essere convertiti in proc con l'operatore &
(che consentirà anche che vengano passati come blocchi).
La classe Symbol definisce #to_proc
così tenta di chiamare il metodo corrispondente sull'oggetto che riceve come parametro.
p [ 'rabbit', 'grass' ].map( &:upcase ) # => ["RABBIT", "GRASS"]
Gli oggetti metodo definiscono anche #to_proc
.
output = method( :p )
[ 'rabbit', 'grass' ].map( &output ) # => "rabbit\ngrass"
Applicazione parziale e Currying
Tecnicamente, Ruby non ha funzioni, ma metodi. Tuttavia, un metodo Ruby si comporta quasi identicamente a funzioni in altre lingue:
def double(n)
n * 2
end
Questo metodo / funzione normale prende un parametro n
, lo raddoppia e restituisce il valore. Ora definiamo una funzione (o metodo) di ordine superiore:
def triple(n)
lambda {3 * n}
end
Invece di restituire un numero, triple
restituisce un metodo. Puoi testarlo usando Interactive Ruby Shell :
$ irb --simple-prompt
>> def double(n)
>> n * 2
>> end
=> :double
>> def triple(n)
>> lambda {3 * n}
>> end
=> :triple
>> double(2)
=> 4
>> triple(2)
=> #<Proc:0x007fd07f07bdc0@(irb):7 (lambda)>
Se vuoi effettivamente ottenere il numero triplicato, devi chiamare (o "ridurre") il lambda:
triple_two = triple(2)
triple_two.call # => 6
O più concisamente:
triple(2).call
Currying e applicazioni parziali
Questo non è utile in termini di definizione di funzionalità di base, ma è utile se si desidera avere metodi / funzioni che non vengono immediatamente chiamati o ridotti. Ad esempio, supponiamo di voler definire metodi che aggiungono un numero per un numero specifico (ad esempio add_one(2) = 3
). Se dovessi definire una tonnellata di questi potresti fare:
def add_one(n)
n + 1
end
def add_two(n)
n + 2
end
Tuttavia, potresti anche fare questo:
add = -> (a, b) { a + b }
add_one = add.curry.(1)
add_two = add.curry.(2)
Usando il calcolo lambda possiamo dire che l' add
è (λa.(λb.(a+b)))
. Il currying è un modo di applicare parzialmente l' add
. Quindi add.curry.(1)
, is (λa.(λb.(a+b)))(1)
che può essere ridotto a (λb.(1+b))
. Applicazione parziale significa che abbiamo passato un argomento da add
ma abbiamo lasciato l'altro argomento da fornire in seguito. L'output è un metodo specializzato.
Altri esempi utili di curry
Diciamo che abbiamo una formula generale davvero grande, che se specifichiamo determinati argomenti ad essa, possiamo ottenere formule specifiche da esso. Considera questa formula:
f(x, y, z) = sin(x\*y)*sin(y\*z)*sin(z\*x)
Questa formula è fatta per lavorare in tre dimensioni, ma diciamo che vogliamo solo questa formula per quanto riguarda y e z. Diciamo anche che per ignorare x, vogliamo impostare il suo valore su pi / 2. Facciamo prima la formula generale:
f = ->(x, y, z) {Math.sin(x*y) * Math.sin(y*z) * Math.sin(z*x)}
Ora, usiamo il curry per ottenere la nostra formula yz
:
f_yz = f.curry.(Math::PI/2)
Quindi per chiamare il lambda memorizzato in f_yz
:
f_xy.call(some_value_x, some_value_y)
Questo è piuttosto semplice, ma diciamo che vogliamo ottenere la formula per xz
. Come possiamo impostare y
su Math::PI/2
se non è l'ultimo argomento? Bene, è un po 'più complicato:
f_xz = -> (x,z) {f.curry.(x, Math::PI/2, z)}
In questo caso, dobbiamo fornire segnaposto per il parametro che non stiamo pre-compilando. Per coerenza possiamo scrivere f_xy
questo modo:
f_xy = -> (x,y) {f.curry.(x, y, Math::PI/2)}
Ecco come funziona il calcolo lambda per f_yz
:
f = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x))))
f_yz = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x)))) (π/2) # Reduce =>
f_yz = (λy.(λz.(sin((π/2)*y) * sin(y*z) * sin(z*(π/2))))
Ora diamo un'occhiata a f_xz
f = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x))))
f_xz = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x)))) (λt.t) (π/2) # Reduce =>
f_xz = (λt.(λz.(sin(t*(π/2)) * sin((π/2)*z) * sin(z*t))))
Per maggiori informazioni sul calcolo lambda, prova questo .