1. 2

Being an Old School Programmer I tend to naturally to write ever more deeply nested loops.

I hate myself when I do this because it’s hard to test especially if some of the loops have nasty external side effects, it’s hard to reuse, it’s hard to refactor.

So I’m trying a new pattern….

Walk with me this is going to be long…. the example is a teaching example / dojo exercise for myself, so excuse me it it slightly contrived.

Here is a typical chunk of my code…

def nested( a, b, c)

   stuff_a = func_a(a)
   stuff_b = func_b(b)
   stuff_c = func_c(c)
   result = {}
   func_1( stuff_a) do |a1|
      stuff_d = func_d( a1 + stuff_b)
      func_2( stuff_d) do |a2|
         stuff_e = func_e( a2 + stuff_c)

         func_3( stuff_e) do |a3|

            result[a3] = func_f( a3)
         end
      end
   end

   result
end

It’s fairly clear but has a few gotchas.

  • It’s unclear which part of the code actually depends on the parameters.
  • In this toy example, the function is small… but a Real Life nested loop function like this can quickly grow hideously large.
  • As a “premature optimization” I have factored out subexpressions that do not alter within the loops, resulting in large scopes for variables that are only used inside the loops.
  • func_1(), func_2(), func_3() yield a stream of things….but a stream of things should just be an enumerable!

Ok, so try 2… reduce the scope of the stuff_* variables, a pessimation..

def nested( a, b, c)

   result = {}
   stuff_a = func_a(a)
   func_1( stuff_a) do |a1|
      stuff_b = func_b(b)
      stuff_d = func_d( a1 + stuff_b)
      func_2( stuff_d) do |a2|
         stuff_c = func_c(c)
         stuff_e = func_e( a2 + stuff_c)

         func_3( stuff_e) do |a3|

            result[a3] = func_f( a3)
         end
      end
   end

   result
end

I can extract the inner loop as a function, but my parameter list balloons…

def inner_2( a2, c, result)
   stuff_c = func_c(c)
   stuff_e = func_e( a2 + stuff_c)

   func_3( stuff_e) do |a3|
      result[a3] = func_f( a3)
   end
end

def nested( a, b, c)

   result = {}
   stuff_a = func_a(a)
   func_1( stuff_a) do |a1|
      stuff_b = func_b(b)
      stuff_d = func_d( a1 + stuff_b)
      func_2( stuff_d) do |a2|
         inner_2( a2, c, result)
      end
   end

   result
end

and I still reevaluate func_c for every loop!

If I use a closure instead, my parameter list collapses again…

def nested( a, b, c)

   result = {}
   stuff_a = func_a(a)
   func_1( stuff_a) do |a1|
      stuff_b = func_b(b)
      stuff_d = func_d( a1 + stuff_b)
      inner_2 = ->( a2){
         stuff_c = func_c(c)
         stuff_e = func_e( a2 + stuff_c)

         func_3( stuff_e) do |a3|
            result[a3] = func_f( a3)
         end
      }
      func_2( stuff_d) do |a2|
         inner_2.call( a2)
      end
   end

   result
end

But I still have a pessimization, so if I could pass an enumerator around…. So lets try convert func_2 to an enumerator….

def func_2( j)
   return to_enum( __method__, j) unless block_given?
   .....lots of code and a...
       yield  a2
   ...lots more code
end

def nested( a, b, c)

   result = {}
   stuff_a = func_a(a)
   func_1( stuff_a) do |a1|
      stuff_b = func_b(b)
      stuff_d = func_d( a1 + stuff_b)
      inner_2 = ->( a2){
         stuff_c = func_c(c)
         stuff_e = func_e( a2 + stuff_c)

         func_3( stuff_e) do |a3|
            result[a3] = func_f( a3)
         end
      }
      func_2( stuff_d).each do |a2|
         inner_2.call( a2)
      end
   end

   result
end

Then pass the enumerator in, and then we can stop the silly re-evaluation of func_c on every loop…

def nested( a, b, c)

   result = {}
   stuff_a = func_a(a)
   func_1( stuff_a) do |a1|
      stuff_b = func_b(b)
      stuff_d = func_d( a1 + stuff_b)
      inner_2 = ->( e){
         stuff_c = func_c(c)

         e.each do |a2|
            stuff_e = func_e( a2 + stuff_c)

            func_3( stuff_e) do |a3|
               result[a3] = func_f( a3)
            end
         end
      }

      inner_2.call( func_2( stuff_d))
   end

   result
end

And I can keep going with func_1….

def nested( a, b, c)

   result = {}
   stuff_a = func_a(a)
   inner_1 = ->( e1, inner_2){
      stuff_b = func_b(b)
      e1.each do |a1|
         stuff_d = func_d( a1 + stuff_b)

         inner_2.call( func_2( stuff_d))
      end
   }

   inner_2 = ->( e2){
      stuff_c = func_c(c)

      e2.each do |a2|
         stuff_e = func_e( a2 + stuff_c)

         func_3( stuff_e) do |a3|
            result[a3] = func_f( a3)
         end
      end
   }


   inner_1.call( func_1( stuff_a), inner_2)


   result
end

I can reduce scope of result and move it to the outermost level….

def nested( a, b, c)

   stuff_a = func_a(a)

   inner_1 = ->( e1, inner_2, &block){
      stuff_b = func_b(b)
      e1.each do |a1|
         stuff_d = func_d( a1 + stuff_b)

         inner_2.call( func_2( stuff_d), &block)
      end
   }

   inner_2 = ->( e2,&block){
      stuff_c = func_c(c)

      e2.each do |a2|
         stuff_e = func_e( a2 + stuff_c)

         func_3( stuff_e,&block)
      end
   }


   result = {}
   inner_1.call( func_1( stuff_a), inner_2) do |a3|
      result[a3] = func_f( a3)
   end
   result
end

I can convert the inner_1 lambda to a vanilla method and convert that to an Enumerator…

def inner_1( b, e1, inner_2, &block)
   return to_enum( __method__, b, e1, inner_2) unless block_given?
   stuff_b = func_b(b)
   e1.each do |a1|
      stuff_d = func_d( a1 + stuff_b)

      inner_2.call( func_2( stuff_d), &block)
   end
end

def nested( a, b, c)

   stuff_a = func_a(a)


   inner_2 = ->( e2,&block){
      stuff_c = func_c(c)

      e2.each do |a2|
         stuff_e = func_e( a2 + stuff_c)

         func_3( stuff_e,&block)
      end
   }


   result = {}
   inner_1( b, func_1( stuff_a), inner_2).each do |a3|
      result[a3] = func_f( a3)
   end
   result
end

Since inner_1 is just a vanilla enum, I can use each_with_object

I can also extract inner_2 as a method that returns a lambda…

def inner_1( b, e1, inner_2, &block)
   return to_enum( __method__, b, e1, inner_2) unless block_given?
   stuff_b = func_b(b)
   e1.each do |a1|
      stuff_d = func_d( a1 + stuff_b)

      inner_2.call( func_2( stuff_d), &block)
   end
end

def inner_2( c)
   stuff_c = func_c(c)

   ->( e2,&block){

      e2.each do |a2|
         stuff_e = func_e( a2 + stuff_c)

         func_3( stuff_e,&block)
      end
   }
end

def nested( a, b, c)

   stuff_a = func_a(a)

   inner_1( b, func_1( stuff_a), inner_2( c)).each_with_object({}) do |a3, result|
      result[a3] = func_f( a3)
   end
end

Note func_c is now only evaluated once again, It’s testable it, it’s reusable in the vanilla “everything is just Enumerable” sense.

  1.  

  2. 1

    I had written a nice thing about how to do this in a functional style… But came away with the conclusion that Ruby is just not good at this. (Based mostly on the fact that the syntax isn’t right to express the ideas needed for a more lisp-like functional programming style).

    Consider some nice named objects that cache the result of func_b on initialization and that do the work you need them to do.

    1. 1

      objects that cache the result of func_b on initialization

      That’s sort of what a closure is…. they are a nifty object that grabs (just) what is needed from the binding environment and caches it for use later.

      Certainly everything I have seen a functional language do I can do in Ruby… it’s more about what parts of Ruby you don’t use than do.

      The other nice thing about enum chains is where appropriate you can drop in “.lazy”…