Ruby Language
Блоки и Procs и Lambdas
Поиск…
Синтаксис
- Proc.new ( block )
- lambda {| args | код}
- -> (arg1, arg2) {code}
- object.to_proc
- {| single_arg | код}
- do | arg, (ключ, значение) | конец кода
замечания
Будьте осторожны с приоритетом оператора, когда у вас есть линия с несколькими прикованными способами, например:
str = "abcdefg"
puts str.gsub(/./) do |match|
rand(2).zero? ? match.upcase : match.downcase
end
Вместо того, чтобы печатать что-то вроде abCDeFg
, как и следовало ожидать, оно печатает что-то вроде #<Enumerator:0x00000000af42b28>
- это потому, что do ... end
имеет более низкий приоритет, чем методы, а это значит, что gsub
видит только аргумент /./
, а не аргумент блока. Он возвращает счетчик. Блок заканчивается передачей puts
, который игнорирует его и просто отображает результат gsub(/./)
.
Чтобы исправить это, либо заверните вызов gsub
в круглые скобки, либо используйте { ... }
.
процедура
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"
Мы скопировали метод call_the_block
из последнего примера. Здесь вы можете видеть, что proc выполняется путем вызова метода proc
с блоком. Вы также можете видеть, что блоки, подобные методам, имеют неявные возвращения, что означает, что procs (и lambdas) тоже делают. В определении its_a
вы можете видеть, что блоки могут принимать аргументы splat, а также обычные; они также могут принимать аргументы по умолчанию, но я не мог придумать, как это работает. Наконец, вы можете видеть, что для вызова метода можно использовать несколько синтаксисов - либо метод call
, либо кнопку []
.
Лямбда
# 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)
Вы также можете использовать ->
для создания и .()
Для вызова лямбда
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
Здесь вы можете видеть, что лямбда почти такая же, как и proc. Однако есть несколько предостережений:
Арктичность аргументов лямбды соблюдается; передавая неправильное количество аргументов в лямбда, поднимет
ArgumentError
. Они могут по-прежнему иметь параметры по умолчанию, параметры splat и т. Д.return
изнутри лямбда возвращается из лямбда, в то время какreturn
из proc возвращается из охватывающей области: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"
Объекты как блок-аргументы для методов
Помещение символа &
(амперсанд) перед аргументом передаст его как блок метода. Объекты будут преобразованы в Proc
используя метод 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)
Это обычная модель в Ruby, и многие стандартные классы предоставляют ее.
Например, Symbol
реализует to_proc
, отправляя себя в аргумент:
# Example implementation
class Symbol
def to_proc
Proc.new do |receiver|
receiver.send self
end
end
end
Это позволяет использовать полезную &:symbol
идиому, обычно используемую с объектами Enumerable
:
letter_counts = %w(just some words).map(&:length) # [4, 4, 5]
Блоки
Блоки представляют собой куски кода, заключенного между фигурными скобками {}
(обычно для однострочных блоков) или do..end
(используется для многострочных блоков).
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
Примечание: фигурные скобки имеют более высокий приоритет, чем do..end
Уступая
Блоки могут использоваться внутри методов и функций, используя 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
Будьте осторожны, хотя, если yield
вызывается без блока, он поднимет значение LocalJumpError
. Для этого ruby предоставляет другой метод block_given?
это позволяет проверить, прошел ли блок перед вызовом доходности
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
может также предлагать аргументы блоку
def yield_n(n)
p = yield n if block_given?
p || n
end
yield_n(12) {|n| n + 7 }
#=> 19
yield_n(4)
#=> 4
Хотя это простой пример , yield
ING может быть очень полезным для обеспечения возможности прямого доступа к переменным экземпляра или оценки внутри контекста другого объекта. Например:
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>
Как вы можете видеть, использование yield
таким образом делает код более читаемым, чем постоянный вызов app.configuration.#method_name
. Вместо этого вы можете выполнить всю конфигурацию внутри блока, поддерживая содержащийся код.
переменные
Переменные для блоков являются локальными для блока (аналогичны переменным функций), они умирают при выполнении блока.
my_variable = 8
3.times do |x|
my_variable = x
puts my_variable
end
puts my_variable
#=> 0
# 1
# 2
# 8
Блоки не могут быть сохранены, они умирают после выполнения. Чтобы сохранить блоки, вам нужно использовать procs
и lambdas
.
Преобразование в Proc
Объекты, которые реагируют на to_proc
могут быть преобразованы в procs с помощью оператора &
(который также позволит им передавать в виде блоков).
Класс Symbol определяет #to_proc
поэтому он пытается вызвать соответствующий метод для объекта, который он получает в качестве параметра.
p [ 'rabbit', 'grass' ].map( &:upcase ) # => ["RABBIT", "GRASS"]
Объекты метода также определяют #to_proc
.
output = method( :p )
[ 'rabbit', 'grass' ].map( &output ) # => "rabbit\ngrass"
Частичное применение и каррирование
Технически Ruby не имеет функций, а методов. Однако метод Ruby почти идентичен функциям на другом языке:
def double(n)
n * 2
end
Этот нормальный метод / функция принимает параметр n
, удваивает его и возвращает значение. Теперь давайте определим функцию (или метод) более высокого порядка:
def triple(n)
lambda {3 * n}
end
Вместо того, чтобы возвращать число, triple
возвращает метод. Вы можете протестировать его с помощью 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)>
Если вы хотите получить тройной номер, вам нужно позвонить (или «уменьшить») лямбда:
triple_two = triple(2)
triple_two.call # => 6
Или более кратко:
triple(2).call
Каррирование и частичное применение
Это не полезно с точки зрения определения очень простых функций, но полезно, если вы хотите иметь методы / функции, которые не вызываются или не вызываются мгновенно. Например, предположим, вы хотите определить методы, которые добавляют число по определенному числу (например, add_one(2) = 3
). Если вам нужно было определить тонну, которую вы могли бы сделать:
def add_one(n)
n + 1
end
def add_two(n)
n + 2
end
Однако вы также можете это сделать:
add = -> (a, b) { a + b }
add_one = add.curry.(1)
add_two = add.curry.(2)
Используя лямбда-исчисление, можно сказать, что add
есть (λa.(λb.(a+b)))
. Currying - это способ частичного применения add
. Итак, add.curry.(1)
, есть (λa.(λb.(a+b)))(1)
которое можно свести к (λb.(1+b))
. Частичное приложение означает, что мы передали один аргумент для add
но оставили другой аргумент, который будет предоставлен позже. Выход является специализированным методом.
Более полезные примеры каррирования
Допустим, у нас действительно большая общая формула, что, если мы укажем для нее некоторые аргументы, мы можем получить от нее конкретные формулы. Рассмотрим эту формулу:
f(x, y, z) = sin(x\*y)*sin(y\*z)*sin(z\*x)
Эта формула предназначена для работы в трех измерениях, но предположим, что мы хотим только эту формулу относительно y и z. Давайте также сказать, что для игнорирования x мы хотим установить его значение в pi / 2. Давайте сначала сделаем общую формулу:
f = ->(x, y, z) {Math.sin(x*y) * Math.sin(y*z) * Math.sin(z*x)}
Теперь давайте использовать currying для получения нашей формулы yz
:
f_yz = f.curry.(Math::PI/2)
Затем, чтобы вызвать лямбду, сохраненную в f_yz
:
f_xy.call(some_value_x, some_value_y)
Это довольно просто, но предположим, что мы хотим получить формулу для xz
. Как мы можем установить y
в Math::PI/2
если это не последний аргумент? Ну, это немного сложнее:
f_xz = -> (x,z) {f.curry.(x, Math::PI/2, z)}
В этом случае нам необходимо предоставить заполнители для параметра, который мы не предварительно заполняем. Для согласованности мы могли бы написать f_xy
следующим образом:
f_xy = -> (x,y) {f.curry.(x, y, Math::PI/2)}
Вот как работает лямбда-исчисление для 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))))
Теперь давайте посмотрим на 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))))
Чтобы узнать больше об исчислении лямбда, попробуйте это .