jueves, 8 de noviembre de 2007

Enumerable#iterate

¿Quién no se ha encontrado alguna vez escribiendo un bucle donde necesita saber cuándo está en el último elemento? ¿O cuál es el próximo elemento?

  coleccion.each {|item|
     if estoy_en_el_ultimo?
       ....

La solución típica en este caso sería usar each_with_index y dentro del bloque coleccion.size == index + 1, pero, siendo Ruby como es, molaría algo más “objetista”

Seguramente habrán muchas implementaciones de algo así, pero me apetecía experimentar con una, así que antes de buscar le di un poco de caña al Enumerable

Partiendo de que en Ruby se puede modificar cualquier cosa (entendiendo por cosa... lo que sea), bastaría con añadir un método iterate al módulo Enumerable, de manera que todos los objetos que actúan como secuencias se beneficiarían del método

Por ejemplo, podríamos tener


output = ''
(0..10).map { (rand * 10).to_i }.iterate {|iterator|
    output << "<b>" if iterator.first? || iterator.last?
    output << iterator.object.to_s

    if iterator.object % 2 == 0
        output << " " << iterator.next.to_s
        iterator.skip_next!
    end

    output << "</b>" if iterator.first? || iterator.last?

    output << " <br />\n" unless iterator.last?
}

puts output


Que nos daría una salida como

<b>6 3</b> <br />
6 2 <br />
7 <br />
3 <br />
2 1 <br />
8 3 <br />
<b>5</b>

O también (un poco más rebuscado)


[1, "uno", 2, "dos", "tres", 3, "cuatro", "cinco", "seis", 0].iterate {|i|
    puts "<div>" if i.first?
    i.skip_next! i.object
    i.object.times {|n| print i.next(n + 1) + " " }
    puts i.last? ? "</div>" : "<br />"
}


Que nos daría

<div>
uno <br />
dos tres <br />
cuatro cinco seis <br />
</div>

¿Y cómo conseguir todo eso?


module Enumerable

    class Iterator
        attr_accessor :object
        attr_accessor :index
        attr_reader :parent

        def initialize(parent)
            @parent = parent
            @skip_items = 0
        end

        def last?
            index == parent.size - 1
        end

        def first?
            index == 0
        end

        def next(offset = 1)
            (offset + index >= parent.size) ? nil : parent[index + offset]
        end

        def previous(offset = 1)
            (index - offset < 0) ? nil : parent[index - offset]
        end

        def skip_next!(items = 1)
            @skip_items += items.to_i
        end

        def skip?(keep_value = false)
            if @skip_items > 0
                @skip_items -= 1 unless keep_value
                true
            end
        end
    end

    def iterate(&block)
        iterator = Iterator.new self
        each_with_index {|object, index|
            next if iterator.skip?
            iterator.object = object
            iterator.index = index
            block.call iterator
        }
    end

end



Después de terminar este pequeño experimento hice una búsqueda rápida a ver qué más había por ahí. Lo único que vi, sin buscar demasiado, fue un mensaje en la ruby-tak del 2002, donde hay una implementación parecida a ésta.