How do you efficiently chain array methods in Ruby

91 Views Asked by At

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
1

There are 1 best solutions below

3
spickermann On

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