0

So let's say I have this MWE code:

# see below for full code - not important here
class RealEstate; attr_accessor :name; attr_accessor :living_space; attr_accessor :devices; def initialize(name, living_space, devices); @name = name; @living_space = living_space; @devices = devices; end; end

real_estates = [ RealEstate.new("ceiling", 30, [1]), # name, living_space, devices
                 RealEstate.new("1st floor", 50, [2,3]),
                 RealEstate.new("Ground floor", 70, [4,5]) ]

(A) Now i would like to use the array methods, especially the pretzel colon like e.g. this:

real_estates.map(&:living_space).inject(:+) # get the sum of all the available living space
real_estates.map(&:devices).map!(&:first) # get the first device of each floor

(B) In my understanding, this seems to be inefficient. The array is processed twice (or multiple times), which has implications in a huge real-world example. I could however write each of this in an own (single) loop:

real_estate.inject(0) do |sum, o|
  sum + o.living_space
end
real_estate.map {|o| o.devices.first}

I would really prefer syntax like in block A over B, but YMMV. I am aware of filter_map or flat_map, which already help in some cases, allegedly improving performance around a factor of 4.5

Especially, when these statements do a lot and get huge, (daisy?) chaining them together seems like a pattern that makes the code readable. Reference: Method Chaining (Idiom): "Train wreck is clean code"


So finally my question: How do you prevent having intermediate results (arrays) and multiple iterations over the same array? Or: how do you do chaining on array methods efficiently?

Rails would be applicable here, but I think there could also be a variant for pure ruby. I imagine something like this:

real_estates.map_inject(&:living_space,:+) # tbh you would need a combination for each of map, select, reject, each, etc.
real_estates.map(&:devices.first)
real_estates.map([&:devices,&:first])

I don't only use map and inject, but also filter, uniq, select, reject (all Enumerable), flatten (Array), etc., also often with a bang


The whole MWE class code:

class RealEstate
  attr_accessor :name
  attr_accessor :living_space
  attr_accessor :devices
  def initialize(name, living_space, devices)
    @name = name
    @living_space = living_space
    @devices = devices
  end
end
3
  • 1
    real_estates.map(&:living_space).inject(:+) can be replaced with real_estates.sum(&:living_space). But for the rest of your question: if your arrays are huge and iterating them has noticeable impact on performance, I would suggest a) restructure your logic to work with smaller arrays (batching/slicing, etc.), b) don't chain methods like that and hand-roll your loops or c) drop ruby for a faster language. Commented Jul 19, 2023 at 9:15
  • 1
    You might want to take a look into Enumerator::Lazy which "allows idiomatic calculations on long or infinite sequences, as well as chaining of calculations without constructing intermediate arrays" Commented Jul 19, 2023 at 10:24
  • If you really want to chain methods, you can override the Symbol #to_proc. This approach however should to my understanding just be syntactic sugar. Commented Jul 19, 2023 at 10:48

1 Answer 1

1

I suggest adding a helper method to your class:

class RealEstate
  attr_accessor :name, :living_space, :devices
  
  def initialize(name, living_space, devices)
    @name = name
    @living_space = living_space
    @devices = devices
  end

  def first_device 
    devices.first
  end
end

Then you can use methods like:

real_estates.sum(&:living_space) # using `sum` as Sergio Tulentsev suggested
real_estates.map(&:first_device) # using the helper method for readability
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for the answer. In my real application, you wouldn't have that exact device array and call .first, but chain through multiple different classes. But I'll look into that idea, possibly I can leverage some delegates in my model! Another common case is .reject(&:nil).map(...) - should I add this to the question?
Hard to give great answers when you do not share your actually problem but just simplified versions... And .reject(&:nil).map(...) can be replaced with filter_map { |x| x&.do_something }
One example that is easy to break down is given in this article. It's easy to understand and basically concludes with rock_hits = [["Queen", "Bohemian Rhapsody"],["Queen", "Don't Stop Me Now"]] (just an excerpt) and rock_hits.group_by(&:shift).transform_values(&:flatten). While this is easy to read, it iterates three times through the same array and I expect it to produce much overhead.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.