Ruby Language
Bloki i Procs i Lambdas
Szukaj…
Składnia
- Proc. Nowa ( blok )
- lambda {| args | kod }
- -> (arg1, arg2) {kod}
- object.to_proc
- {| single_arg | kod }
- do | arg, (klucz, wartość) | koniec kodu
Uwagi
Uważaj na pierwszeństwo operatora, gdy masz linię z wieloma metodami, na przykład:
str = "abcdefg"
puts str.gsub(/./) do |match|
rand(2).zero? ? match.upcase : match.downcase
end
Zamiast abCDeFg
coś takiego jak abCDeFg
, jak można się spodziewać, wypisuje coś takiego: #<Enumerator:0x00000000af42b28>
- to dlatego do ... end
że do ... end
ma niższy priorytet niż metody, co oznacza, że gsub
widzi tylko argument /./
, a nie argument blokowy. Zwraca moduł wyliczający. Blok jest ostatecznie przekazywany do puts
, co go ignoruje i wyświetla wynik gsub(/./)
.
Aby to naprawić, zawiń wywołanie gsub
w nawiasach lub zamiast tego użyj { ... }
.
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"
Skopiowaliśmy metodę call_the_block
z ostatniego przykładu. Tutaj możesz zobaczyć, że proc jest tworzony przez wywołanie metody proc
z blokiem. Możesz także zobaczyć, że bloki, podobnie jak metody, mają niejawne zwroty, co oznacza, że procs (i lambdas) również. W definicji its_a
widać, że bloki mogą przyjmować argumenty splat, jak również zwykłe; są również zdolne do przyjmowania domyślnych argumentów, ale nie mogłem wymyślić sposobu, aby to wykorzystać. Na koniec widać, że można użyć wielu składni do wywołania metody - albo metody call
, albo []
operator.
Lambdas
# 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)
Możesz także użyć ->
aby utworzyć i .()
Aby wywołać 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
Tutaj możesz zobaczyć, że lambda jest prawie taka sama jak proc. Istnieje jednak kilka zastrzeżeń:
Egzekwowanie argumentów lambdy jest egzekwowane; przekazanie niewłaściwej liczby argumentów do lambda spowoduje podniesienie
ArgumentError
. Nadal mogą mieć parametry domyślne, parametry splat itp.return
z wewnątrz lambda zwraca z lambda, podczas gdyreturn
z proc zwraca z zakresu obejmującego: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"
Obiekty jako argumenty blokowe dla metod
Umieszczenie znaku &
(ampersand) przed argumentem przekaże go jako blok metody. Przedmioty zostaną przekształcone do Proc
pomocą to_proc
metody.
class Greeter
def to_proc
Proc.new do |item|
puts "Hello, #{item}"
end
end
end
greet = Greeter.new
%w(world life).each(&greet)
Jest to powszechny wzorzec w Ruby i zapewnia go wiele standardowych klas.
Na przykład Symbol
s implementuje to_proc
, wysyłając się do argumentu:
# Example implementation
class Symbol
def to_proc
Proc.new do |receiver|
receiver.send self
end
end
end
Umożliwia to użyteczny idiom &:symbol
Enumerable
często używany w obiektach Enumerable
:
letter_counts = %w(just some words).map(&:length) # [4, 4, 5]
Bloki
Bloki to fragmenty kodu zamknięte między nawiasami klamrowymi {}
(zwykle dla bloków do..end
) lub do..end
(używane dla bloków wieloliniowych).
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
Uwaga: nawiasy klamrowe mają wyższy priorytet niż do..end
Wydajność
Bloki mogą być używane w metodach i funkcjach za pomocą słowa 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
Bądź jednak ostrożny, jeśli yield
jest nazywany bez bloku to podniesie się LocalJumpError
. W tym celu Ruby udostępnia inną metodę o nazwie block_given?
pozwala to sprawdzić, czy blok został przekazany przed wywołaniem dochodu
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
może również dostarczać argumenty do bloku
def yield_n(n)
p = yield n if block_given?
p || n
end
yield_n(12) {|n| n + 7 }
#=> 19
yield_n(4)
#=> 4
Chociaż jest to prosty przykład, yield
może być bardzo przydatne do umożliwienia bezpośredniego dostępu do zmiennych instancji lub ocen wewnątrz kontekstu innego obiektu. Na przykład:
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>
Jak widać używając yield
w ten sposób sprawia, że kod jest bardziej czytelny niż nieustannie dzwoni app.configuration.#method_name
. Zamiast tego możesz wykonać całą konfigurację wewnątrz bloku, zachowując kod.
Zmienne
Zmienne dla bloków są lokalne dla bloku (podobne do zmiennych funkcji), umierają, gdy blok jest wykonywany.
my_variable = 8
3.times do |x|
my_variable = x
puts my_variable
end
puts my_variable
#=> 0
# 1
# 2
# 8
Bloki nie mogą być uratowane, giną po wykonaniu. Aby zapisać bloki, musisz użyć procs
i lambdas
.
Konwertowanie na Proc
Obiekty, które odpowiadają na to_proc
można przekonwertować na procs za pomocą operatora &
(co pozwoli również na przekazywanie ich jako bloków).
Symbol klasy definiuje #to_proc
więc próbuje wywołać odpowiednią metodę na obiekcie, który otrzymuje jako parametr.
p [ 'rabbit', 'grass' ].map( &:upcase ) # => ["RABBIT", "GRASS"]
Obiekty metod definiują również #to_proc
.
output = method( :p )
[ 'rabbit', 'grass' ].map( &output ) # => "rabbit\ngrass"
Częściowe zastosowanie i curry
Technicznie, Ruby nie ma funkcji, ale metody. Jednak metoda Ruby zachowuje się prawie identycznie jak funkcje w innym języku:
def double(n)
n * 2
end
Ta normalna metoda / funkcja przyjmuje parametr n
, podwaja go i zwraca wartość. Teraz zdefiniujmy funkcję (lub metodę) wyższego rzędu:
def triple(n)
lambda {3 * n}
end
Zamiast zwracać liczbę, triple
zwraca metodę. Możesz to przetestować za pomocą interaktywnej powłoki Ruby :
$ 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)>
Jeśli chcesz faktycznie uzyskać trzykrotny numer, musisz wywołać (lub „zmniejszyć”) lambda:
triple_two = triple(2)
triple_two.call # => 6
Lub bardziej zwięźle:
triple(2).call
Curry i częściowe zastosowania
Nie jest to przydatne w kontekście definiowania bardzo podstawowej funkcjonalności, ale jest przydatne, jeśli chcesz mieć metody / funkcje, które nie są natychmiast wywoływane lub redukowane. Załóżmy na przykład, że chcesz zdefiniować metody, które dodają liczbę według określonej liczby (na przykład add_one(2) = 3
). Gdybyś musiał zdefiniować tonę tych, możesz zrobić:
def add_one(n)
n + 1
end
def add_two(n)
n + 2
end
Możesz jednak również to zrobić:
add = -> (a, b) { a + b }
add_one = add.curry.(1)
add_two = add.curry.(2)
Za pomocą rachunku lambda możemy powiedzieć, że add
to (λa.(λb.(a+b)))
. Curry to sposób na częściowe zastosowanie add
. Więc add.curry.(1)
, to (λa.(λb.(a+b)))(1)
które można sprowadzić do (λb.(1+b))
. Częściowa aplikacja oznacza, że przekazaliśmy jeden argument do add
ale pozostawiliśmy drugi argument do podania później. Wynik jest metodą specjalistyczną.
Bardziej przydatne przykłady curry
Powiedzmy, że mamy naprawdę dużą ogólną formułę, że jeśli podamy jej określone argumenty, możemy uzyskać z niej określone formuły. Rozważ tę formułę:
f(x, y, z) = sin(x\*y)*sin(y\*z)*sin(z\*x)
Ta formuła jest przeznaczona do pracy w trzech wymiarach, ale powiedzmy, że chcemy tej formuły tylko w odniesieniu do yi z. Powiedzmy również, że aby zignorować x, chcemy ustawić jego wartość na pi / 2. Najpierw utwórzmy ogólną formułę:
f = ->(x, y, z) {Math.sin(x*y) * Math.sin(y*z) * Math.sin(z*x)}
Teraz użyjmy curry, aby uzyskać naszą formułę yz
:
f_yz = f.curry.(Math::PI/2)
Następnie, aby wywołać lambda przechowywaną w f_yz
:
f_xy.call(some_value_x, some_value_y)
Jest to dość proste, ale powiedzmy, że chcemy uzyskać wzór na xz
. Jak ustawić y
na Math::PI/2
jeśli nie jest to ostatni argument? Cóż, jest to trochę bardziej skomplikowane:
f_xz = -> (x,z) {f.curry.(x, Math::PI/2, z)}
W takim przypadku musimy podać symbole zastępcze dla parametru, który nie jest wstępnie wypełniany. Dla spójności możemy napisać f_xy
następujący sposób:
f_xy = -> (x,y) {f.curry.(x, y, Math::PI/2)}
Oto jak działa rachunek lambda dla 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))))
Teraz spójrzmy na 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))))
Aby uzyskać więcej informacji na temat rachunku lambda, spróbuj tego .