Iterátory v Ruby jsou ve spojení s uzávěry (closures) velmi silným nástrojem. Každá třída, která reprezentuje nějakou kolekci, totiž obsahuje Enumerable modul. Ten s sebou přináší několik iterátorů. Jednoduchá iterace přes položky pole vypadá takto:
an_array.each do |item| # udelej neco s item end
Mějme nějaká dvourozměrná data, která nejsou polem. Například objekt obsahující souřadnice mezních bodů polygonu (pravý horní, levý horní, pravý dolní a levý dolní).
# polygon0.rb class Polygon attr_accessor :ur_x, :ur_y, :ul_x, :ul_y, :lr_x, :lr_y, :ll_x, :ll_y end
Takto vypadá jednoduchá metoda, která hodnoty všech polí vypíše do konzole:
# polygon0.rb
def print_corners
puts ur_x, ur_y, ul_x, ul_y, lr_x, lr_y, ll_x, ll_y
end
Stačilo zkopírovat seznam polí a odstranit dvojtečky. Kopírování kódu ale zavání problémem, takže je lepší se hned vrhnout do refaktoringu a seznam rohů si uložit do konstanty a upravit související kód.
# polygon1.rb
class Polygon
CORNERS = [:ur_x, :ur_y, :ul_x, :ul_y, :lr_x, :lr_y, :ll_x, :ll_y]
CORNERS.each do |corner|
attr_accessor corner
end
def print_corners
CORNERS.each do |corner|
puts send(corner)
end
end
end
Volání send(corner) je dynamickým voláním metody, tady snad není nutné další vysvětlení. Takto přepsaná třída dělá to samé, co předchozí verze, ale je daleko rozšiřitelnější.
Aby se dalo přes data iterovat skutečně dvourozměrně, musíme konstanty ještě trošku poupravit.
# polygon2.rb
class Polygon
CORNERS = ["ur", "ul", "lr", "ll"]
PARTS = ["x", "y"]
CORNERS.each do |corner|
PARTS.each do |part|
attr_accessor "#{corner}_#{part}".to_sym
end
end
def print_corners
CORNERS.each do |corner|
PARTS.each do |part|
puts send("#{corner}_#{part}".to_sym)
end
end
end
end
To na jednu stranu přidává ohromnou flexibilitu při práci s jednotlivými souřadnicemi, na druhou stranu přináší nutnost pro každou operaci kopírovat dva vnořené iterátory. Pro příklad přidám metodu, která zkontroluje, zda jsou všechna pole nulová a pokud ano, nastaví je na nil.
# polygon2.rb
def fix_zero_to_nil
all_zero = true
CORNERS.each do |corner|
PARTS.each do |part|
val = send("#{corner}_#{part}".to_sym)
all_zero &&= val.nil? || val == 0
end
end
return unless all_zero
CORNERS.each do |corner|
PARTS.each do |part|
send("#{corner}_#{part}=".to_sym, nil)
end
end
end
Na tomto kódu je krásně vidět, jak metoda díky nutnosti pokaždé iterovat přes dvě pole nabobtnala. Je třeba znova refaktorovat a udělat si vlastní iterátory. Jeden pro případ, že nás skutečně zajímá iterátor pro každou úroveň. Druhý pro případ, že nás zajímají jenom spojené položky.
# polygon3.rb
def Polygon.each_corner_and_part
CORNERS.each do |corner|
PARTS.each do |part|
yield(corner, part)
end
end
end
def Polygon.each_coordinate
each_corner_adn_part do |corner, part|
yield("#{corner}_#{part}")
end
end
Tyto iterátory přijdou vhod ve všech metodách třídy:
# polygon3.rb
class Polygon
CORNERS = ["ur", "ul", "lr", "ll"]
PARTS = ["x", "y"]
def Polygon.each_corner_and_part
CORNERS.each do |corner|
PARTS.each do |part|
yield(corner, part)
end
end
end
def Polygon.each_coordinate
each_corner_and_part do |corner, part|
yield("#{corner}_#{part}")
end
end
each_coordinate do |coord|
attr_accessor coord.to_sym
end
def print_corners
Polygon.each_coordinate do |coord|
puts send(coord.to_sym)
end
end
def fix_zero_to_nil
all_zero = true
Polygon.each_coordinate do |coord|
val = send(coord.to_sym)
all_zero &&= val.nil? || val == 0
end
return unless all_zero
Polygon.each_coordinate do |coord|
send("#{coord}=".to_sym, nil)
end
end
end
Zdorjové kódy jsou k dispozici na githubu: refaktoring-iteratoru
Timy, 22.2.2009, 21:27
Nevím jak vy Rubysti, ale my Lispaři říkáme “Closure” česky “uzávěr”
. IMHO je to hezčí než ten anglický výraz.