Search code examples
rubydesign-patternsidiomsmutation

Is there an abstraction for the declare-update-return pattern?


When writing iterative code with mutation in ruby, I often find myself following this pattern:

def build_x some_data
  x = [] # or x = {}
  some_data.each do |data|
    x.some_in_place_update! (... data ...)
  end
  x
end

(x often does not have the same shape as some_data, so a simple map will not do.)

Is there a more idiomatic or better way to write code that follows this pattern?


[edit] A real example:

def to_hierarchy stuff
  h = {}
  stuff.each do |thing|
    path = thing.uri.split("/").drop(4)
    sub_h = h
    path.each do |segment|
      sub_h[segment] ||= {}
      sub_h = sub_h[segment]
    end
    sub_h.merge!(
      data: thing.data,
    )
  end
  h
end

This begins with a flat list of things, which have related but distinct uris. It transforms this flat list into a hierarchy, grouping related things that share the same segments of a uri. This follows the pattern I described: initialize h, loop over some data and mutate h along the way, and then spit out h at the end.

[edit2] Another related example

def count_data obj
  i = if obj[:data] then 1 else 0
  obj.each do |k, v|
    i += count_statements v unless :data == k
  end
  i
end

Solution

  • Your to_hierarchy example could be done with each_with_object:

    def to_hierarchy stuff
      stuff.each_with_object({}) do |thing, h|
        #...
      end
    end
    

    each_with_object passes the extra object to the block and returns that object when the iteration is done.

    If you're more of a traditionalist, you could use inject:

    def to_hierarchy stuff
      stuff.inject({}) do |h, thing|
        #...
        h
      end
    end
    

    Note the block argument order change and that the block has to return h so that inject can feed it back into the next block invocation.

    Your general example could be written as:

    def build_x some_data
      some_data.each_with_object([]) do |data, x|
        x.some_in_place_update! (... data ...)
      end
    end
    

    or:

    def build_x some_data
      some_data.inject({}) do |x, data|
        x.some_in_place_update! (... data ...)
        x
      end
    end