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
sb813322
  • 129
  • 9
  • 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. – Sergio Tulentsev Jul 19 '23 at 09:15
  • 1
    You might want to take a look into [`Enumerator::Lazy`](https://ruby-doc.org/3.2.2/Enumerator/Lazy.html) which _"allows idiomatic calculations on long or infinite sequences, as well as chaining of calculations without constructing intermediate arrays"_ – Stefan Jul 19 '23 at 10:24
  • If you really want to chain methods, you can [override the Symbol `#to_proc`](https://stackoverflow.com/a/36352944/4575793). This approach however should to my understanding just be [syntactic sugar](https://en.wikipedia.org/wiki/Syntactic_sugar). – Cadoiz Jul 19 '23 at 10:48

1 Answers1

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
spickermann
  • 100,941
  • 9
  • 101
  • 131
  • 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? – sb813322 Jul 19 '23 at 09:52
  • 4
    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 }` – spickermann Jul 19 '23 at 11:43
  • One example that is easy to break down is given in [this article](https://jelera.github.io/howto-work-with-ruby-group-by). 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. – sb813322 Aug 11 '23 at 07:41