Нумераторы, итераторы, лямбды
Андрей Васильев
2019
Нумераторы - объекты-итераторы
- Итераторы предлагают способ взаимодействия с внутренним состоянием объекта через вызов методов
- Очень сложно таким образом решить задачу синхронной обработки набора коллекций
- Иногда интересно передать итератор внутрь другого метода
- Класс
Enumerator
реализует «внешние итераторы»
a = [1, 3, "cat"]
h = {dog: "canine", fox: "vulpine"}
# Создаём нумераторы
enum_a = a.to_enum
enum_h = h.to_enum
enum_a.next # => 1
enum_h.next # => [:dog, "canine"]
enum_a.next # => 3
enum_h.next # => [:fox, "vulpine"]
Создание нумератора
- Вызов метода
to_enum
на коллекции или enum_for
с указанием названия итератора
- Большинство стандартных итераторов возвращают нумераторы, если с ними не ассоциирован блок
a = [1, 3, "cat"]
enum_a = a.each # Созадаём нумератор
enum_a.next # => 1
enum_b = a.to_enum # Создаём нумератор
enum_c = a.enum_for(:each) # Создаём нумератор
Нумераторы являются объектами класса Enumerator
, который включает в себя модуль Enumerable
, что делает доступным для нумераторов всех «классных» методов
Метод loop
Задачей данного метода является бесконечный вызов блока. Если внутри блока используются нумераторы, то выход будет осуществлён, когда закончатся значения в нумераторе
short = [1, 2, 3].to_enum
long = ('a'..'z').to_enum
loop do
puts " #{short.next} - #{long.next}"
end
Нумераторы являются объектами
Метод each_with_index
определён в модуле Enumerable
result = []
['a', 'b', 'c'].each_with_index do |item, index|
result << [item, index]
end
result # => [["a", 0], ["b", 1], ["c", 2]]
Метод with_index
определён в классе Enumerator
result = []
"dog".each_char.with_index do |item, index|
result << [item, index]
end
result # => [["d", 1], ["o", 2], ["g", 2]]
Создание нумераторов с enum_for
Методу enum_for
можно передать название метода-итератора, который будет предоставлять значения последовательности
enum = "cat".enum_for(:each_char)
enum.to_a # => ["c", "a", "t"]
Если итератор ожидает аргументов, то их следует передать после имени метода
enum_in_threes = (1..7).enum_for(:each_slice, 3)
enum_in_threes.to_a # => [[1, 2, 3], [4, 5, 6],
# [7]]
Создание произвольных нумераторов
Нумераторы могут быть созданы на основе обычного блока, который предоставляет очередные значения
numbers = Enumerator.new do |yielder|
number = 0
count = 1
loop do
number += count
count += 1
yielder.yield number
end
end
5.times { print numbers.next, " " }
puts numbers.first(5) # Доступны методы Enumerable
Бесконечные последовательности
Если генерирующий блок способен предоставлять бесконечное число значений, то его надо указать “ленивым”
numbers = Enumerator.new do |yielder|
number = 0
loop do
number += 1
yielder.yield number
end
end.lazy
puts numbers.all.first(10)
puts numbers.select { |val|
val % 10 == 0 }.first(5)
puts numbers.select { |val|
(val % 3).zero? }.first(10)
Создание собственного нумератора
Хорошей практикой при создании собственного итератора является возвращение нумератора в случае, когда блок не ассоциирован с данным методом
def iterator
return enum_for(:iterator) unless block_given?
...
end
В результате ваш собственный итератор можно будет использовать как встроенные итераторы: либо в форме ассоциации с болоком, либо в форме получения нумератора
some = Some.new
some.iterator { ... }
numer = some.iterator
numer.each { ... }
Блоки для описания транзакций
Зачастую необходимо выполнять связные действия, например открытый файл обязательно должне быть закрыт
class File
def self.open_and_process(*args)
f = File.open(*args)
yield f
f.close()
end
end
File.open_and_process("testfile", "r") do |file|
while line = file.gets
puts line
end
end
Интересные моменты из примера
- Методы, начинающиеся со слова
self
, относятся к классу, а не к объекту класса (“статические”)
*args
в аргументах метода open_and_process
собирает все аргументы в массив args
*args
в вызове метода open
раскрывает содержимое массива и записывает их как аргументы метода
- Есть также нотация
**opts
для обработки именованных аргументов
- Вы можете открыть существующие классы и добавить в них методы. Данная техника на настоящий момент не приветствуется, так как классы - глобальные переменные. Используйте наследование, если это необходимо.
Метод File.open
- Данный метод уже реализует необходимую функциональность, вы можете ассоциировать с ним блок
- Метод
block_given?
проверяет наличие блока и позволяет реализовать альтернативное поведение
if block_given?
result = yield file
file.close
Данная техника применяется в итераторах Array, Hash, Enumerable и т.д. для обработки ситуации работы метода с блоком и без него. Если вы не ассоциировали блок с итератором, то он вернёт нумератор
Блоки могут быть объектами
Блоки похожи на анонимные методы, однако с ними можно общаться как с объектами: сохранять в переменные…
class ProcExample
def pass_in_block(&action)
@stored_proc = action
end
def use_proc(parameter)
@stored_proc.call(parameter)
end
end
eg = ProcExample.new
eg.pass_in_block do |param|
puts "The parameter is #{param}"
end
eg.use_proc(99)
Создание блоков-объектов
Блоки представлены классом Proc
reach = Proc.new do |param|
puts "You called #{param}"
end
reach.call(42) # => You called 42
reach.call('scar') => You called scar
Объекты можно вернуть из методов
def create_block_object(&block)
block
end
reach = create_block_object do |param|
puts "You called #{param}"
end
Лямбда-блоки
Блоки можно создавать с помощью метода lambda
bo = lambda do |param1, param2|
puts "You called me with #{param1}"
end
bo.call(42, 'reason')
Или использовать краткий синтаксис ->
bo = -> (param1, param2) do
puts "You called me with #{param1}"
end
bo.call('dog', 'barks')
Отличие лямбд от блоков
- При вызове лямбды проверяется количество аргументов
- При вызове блока аргументы обрабатываются так
- Если блок был вызван с 1 аргументом - массивом, тогда его значения становятся значением параметров блока
- Если блоку было передано меньше аргументов, чем нужно, то оставшиеся аргументы будут равны
nil
- Если блоку было передано больше аргументов, чем нужно, то оставшиеся параметры будут отброшены
вывод Используйте лямбды, т.к. они предоставляют простую модель использования и гарантии
- Внутри тела лямбды ключевое слово
return
выходит из лямбды, а не из метода, её вызвавшую.
- Внутри тела блока
return
приведёт к выбросу исключения, если будет вызвано вне связанного метода
вывод Не используйте ключевое слово return
внутри блоков
Вызов лямбд и блоков
Лямбды и блоки определены в едином классе Proc
Для вызова блока он предоставляет следующие методы:
action = -> (param) { ... }
#call(params)
: action.call(42)
#.(params)
: action.(42)
#[params]
: action.[42]
#yield(params)
: action.yield(42)
Все формы равносильны между собой. Рекомендуется использовать #call()
Замыкания в блоках
Замыкание - возможность доступа к переменным, объявлённых вне блока
def n_times(thing)
lambda {|n| thing * n }
end
p1 = n_times(23)
p1.call(3) # => 69
p1.call(2) # => 46
- Аргумент
thing
метода n_times
находится в области видимости выражений блока
- При вызове лямбды происходит обращение к
thing
- Вы можете изменять такие переменные, но осторожно
Генераторы на основе лямбд
Можно реализовать простой генератор следующим образом
def power_proc_generator
value = 1
lambda { value += value }
end
power_proc = power_proc_generator
puts power_proc.call # => 2
puts power_proc.call # => 4
Синтаксис для создания лямбд
Современным способом описания коротких лямбд является
-> params { ... }
proc1 = -> arg {puts "In proc1 with #{arg}"}
- Список аргументов выносится перед телом блока
- Ключевое слово
lambda
длиннее ->
def my_while(cond, &body)
while cond.call
body.call
end
end
a = 0
my_while -> { a < 3 } do
puts a
a += 1
end
Преобразование объектов в блоки
Любой класс, реализующий метод #to_proc
может быть преобразован в блок с помощью оператора &
class Greater
def initialize(greating)
@greating = greating
end
def to_proc
proc {|name| "#{@greating}, #{name}!" }
end
end
hi = Greater.new("Hi")
hey = Greater.new("Hey")
["Bob", "Jane"].map(&hi) #=> ["Hi, Bob!", "Hi, Jane!"]
["Bob", "Jane"].map(&hey) #=> ["Hey, Bob!", "Hey, Jane!"]
Поддержка со стороны стандартных классов
Символы
Создаёт блок, который будет вызывать метод с именем символа
:to_s.to_proc.call(1) #=> "1"
[1, 2].map(&:to_s) #=> ["1", "2"]
(1..3).map(&:succ) #=> [2, 3, 4]
Хеши
Создаёт блок, который связывает ключи с их значением
h = {a:1, b:2}
hash_proc = h.to_proc
hash_proc.call(:a) #=> 1
hash_proc.call(:b) #=> 2
hash_proc.call(:c) #=> nil
[:a, :b, :c].map(&h) #=> [1, 2, nil]
Блоки для вызова методов объектов
Метод Object#method(symbol)
позволяет создать объект класса Method
, который позволяет вызывать данный метод в стиле лямбды
Интерфейс класса Method
повторяет в ключевых моментах интерфейс класса Proc
и де-факто может использоваться как альтернатива блокам и лямбдам
class Thing
def square(n)
n*n
end
end
thing = Thing.new
square_method = thing.method(:square)
square_method.call(9) #=> 81
[ 1, 2, 3 ].map(&square_method) #=> [1, 4, 9]
Композиция лямбд (с версии 2.6)
Классы Proc
и Method
предоставляют методы <<
и >>
, которые позволяют создать цепочки обработки данных
multiplication = lambda {|x| x * x }
addition = lambda {|x| x + x }
(multiplication << addition).call(2) #=> 16
(multiplication >> addition).call(2) #=> 8
Ограничением такого подхода является то, чтобы формат передаваемых данных был в точности тем, что ожидают блоки
result = multiplication.call(2).then do |mult|
addition.call(mult)
end
puts result #=> 8
Каррирование блоков
Объекты классов Proc
и Method
поддерживают метод curry
, который позволяет создавать блоки с предустановленными аргументами
summator = lambda { |x, y, z| x + y + z }
summator.curry.call(1).call(2).call(3) #=> 6
apply_math = -> (fn, a, b) { a.send(fn), b }
add = apply_math.curry.(:+)
add.(1, 2) # => 3
increment = add.curry.(1)
increment.(1) # => 2
increment.(5) # => 6