Программирование на языке Ruby - Хэл Фултон
Шрифт:
Интервал:
Закладка:
def each
@store.each {|k,v| v.each {|y| yield k,y}}
end
alias each_pair each
def each_key
self.keys.each {|k| yield k}
end
def each_value
self.values.each {|v| yield v}
end
def has_key? k
self.keys.include? k
end
def has_value? v
self.values.include? v
end
def length
self.values.size
end
alias size length
def delete k
val = @store[k]
@store.delete k
val
end
def delete k,v
@store[k] -= [v] if @store[k]
v
end
# Остальные методы опущены...
end
# He будет работать... для повторяющихся ключей
# актуально только последнее значение.
h = {1=>1, 2=>4, 3=>9, 4=>16, 2=>0}
# А так будет...
h = HashDup.new(1,1, 2,4, 3,9, 4,16, 2,0)
k = h.keys # [4, 1, 2, 2, 3]
v = h.values # [16, 1, 4, 0, 9]
n = h.size # 5
h.each {|k,v| puts "#{k} => #{v}"}
# Печатается:
# 4 => 16
# 1 => 1
# 2 => 4
# 2 => 0
# 3 => 9
Но если не пользоваться литеральными хэшами, то задача решаема. В листинге 8.1 реализован класс, содержащий атрибут @store, который является обычным хэшем; каждое значение этого хэша представляет собой массив. Доступ к хэшу организован так, что при необходимости добавить ключ, который уже существует, мы на самом деле добавляем новое значение в массив, ассоциированный с этим ключом.
Что должен возвращать метод size? Очевидно, «истинное» число пар ключ-значение, включая и дубликаты. Аналогично метод keys возвращает массив, который может содержать дубликаты. Итераторы ведут себя естественно; как и в случае обычного хэша, порядок обхода непредсказуем.
Помимо стандартного метода delete мы реализовали метод delete_pair. Первый удаляет все значения, ассоциированные с данным ключом, второй — только конкретную пару ключ-значение. (Отметим, что было бы затруднительно реализовать единственный метод вида delete(k, v=nil), так как nil — допустимое значение в любом хэше.)
Для краткости мы не стали реализовывать весь класс целиком и, честно говоря, для некоторых методов, например invert, пришлось бы принимать небанальные решения по поводу желательного поведения. Интересующийся читатель может восполнить пробелы.
8.3. Перечисляемые структуры в общем
Что делает набор перечисляемым? Вообще-то сам тот факт, что это набор. Модуль Enumerable требует, чтобы был определен стандартный итератор each. Последовательность обхода не имеет значения, так как даже неупорядоченные наборы, например хэш, могут обладать итераторами.
Кроме того, если предполагается пользоваться методами min, max и sort, то для набора должен быть определен метод сравнения (<=>). Все это достаточно очевидно.
Итак, перечисляемая структура представляет собой набор, в котором можно производить поиск, который можно обойти и, быть может, отсортировать. В любой определенный пользователем набор, не являющийся подклассом существующего системного класса, имеет смысл подмешивать модуль Enumerable.
Имейте в виду — все сказанное о какой-то одной перечисляемой структуре относится ко всем. В качестве примеров таких структур можно назвать массив, хэш, дерево и т.д.
Конечно, у каждой структуры есть свои нюансы. Массив — это упорядоченный набор отдельных элементов, а хэш — неупорядоченный набор пар ключ-значение. Понятно, что в каких-то отношениях они будут вести себя по-разному.
Многие методы, с которыми мы познакомились при изучении массивов и хэшей (например, map и find), на самом деле определены в модуле Enumerable. Часто было трудно решить, как подать материал. Любая путаница или неточность — моя вина!..
Массив — наиболее часто употребляемый набор, подмешивающий этот модуль. Поэтому по умолчанию я буду пользоваться в примерах именно массивами.
8.3.1. Метод inject
Метод inject пришел в Ruby из языка Smalltalk (впервые он появился в версии Ruby 1.8). Его поведение интересно, хотя с первого раза понять его нелегко.
Он отражает тот факт, что мы часто хотим обойти список и по ходу «аккумулировать» некоторый результат. Конечно, самый естественный пример — суммирование чисел в списке. Но и для других операций обычно есть некий «аккумулятор» (которому присваивается начальное значение) и применяемая функция (в Ruby она представлена блоком).
В качестве тривиального примера рассмотрим массив чисел, которые нужно просуммировать:
nums = [3,5,7,9,11,13]
sum = nums.inject(0) {|x,n| x+n }
Обратите внимание, что начальное значение аккумулятора равно 0 («нейтральный элемент» для операции сложения). Затем блок получает текущее значение аккумулятора и значение текущего элемента списка. Действие блока заключается в прибавлении нового значения к текущей сумме.
Ясно, что этот код эквивалентен следующему:
sum = 0
nums.each {|n| sum += n }
В данном случае уровень абстракции лишь немногим выше. Если идея метода inject не укладывается у вас в голове, не пользуйтесь им. Но если удалось преодолеть первоначальное непонимание, то вы сможете найти ему новые элегантные применения.
Начальное значение аккумулятора задавать необязательно. Если оно опущено, то в качестве такового используется значение первого элемента, который при последующих итерациях пропускается,
sum = nums.inject {|x,n| x+n }
# To же самое, что:
sum = nums[0]
nums[1..-1].each {|n| sum + = n }
Другой похожий пример — вычисление произведения чисел. В данном случае аккумулятору следует присвоить начальное значение 1 (нейтральный элемент для операции умножения).
prod = nums.inject(1) {|x,n| x*n }
# или
prod = nums.inject {|x,n| x*n }
В следующем немного более сложном примере мы находим самое длинное слово в списке:
words = %w[ alpha beta gamma delta epsilon eta theta ]
longest_word = words.inject do |best,w|
w.length > best.length ? w : best
end
# Возвращается значение "epsilon".
8.3.2. Кванторы
Кванторы any? и all? появились в версии Ruby 1.8, чтобы было проще проверять некоторые свойства набора. Оба квантора принимают в качестве параметр блок (который должен возвращать значение true или false).
Nums = [1,3,5,8,9]
# Есть ли среди чисел четные?
flag1 = nums.any? {|x| x % 2 == 0 } # true
# Все ли числа четные?
flag2 = nums.all? {|x| x % 2 == 0 } # false
Если блок не задан, то просто проверяется значение истинности каждого элемента. Иными словами, неявно добавляется блок {|x| x }.
flag1 = list.all? # list не содержит ни одного false или nil.
flag1 = list.any? # list содержит хотя бы одно истинное значение
# не nil и не false).
8.3.3. Метод partition
Как говорится, «в мире есть два сорта людей: те, что делят людей по сортам, и те, что не делят». Метод partition относится не к людям (хотя мы можем представить их в Ruby как объекты), но тоже делит набор на две части.
Если при вызове partition задан блок, то он вычисляется для каждого элемента набора. В результате создаются два массива: в первый попадают элементы, для которых блок вернул значение true, во второй — все остальные. Метод возвращает массив, двумя элементами которого являются эти массивы.
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]
odd_even = nums.partition {|x| x % 2 == 1 }
# [[1,3,5,7,9],[2,3,4,6,8]]
under5 = nums.partition {|x| x < 5 }
# [[1,2,3,4],[5,6,7,8,9]]
squares = nums.partition {|x| Math.sqrt(x).to_i**2 == x }
# [[1,4,9], [2,3,5,6,7,8]]
Если нужно разбить набор больше чем на две группы, придется написать собственный метод. Я назвал его classify по аналогии с методом из класса Set.
module Enumerable
def classify(&block)
hash = {}
self.each do |x|
result = block.call(x)
(hashfresult] ||= []) << x
end
hash
end
end
nums = [1,2,3,4,5,6,7,8,9]
mod3 = nums.classify {|x| x % 3 }
# { 0=>[3,6,9], 1=>[1,4,7], 2=>[2,5,8] }
words = %w( area arboreal brick estrous clear donor ether filial
patina ]
vowels = words.classify {|x| x.count("aeiou") }
# {1=>["brick"], 2=>["clear", "donor", "ether"],