Re: Thinking Aloud: Refactoring from nested loops to enumerators and lambda's
Brandon Weaver <[email protected]> Sun, 30 Jun 2019 23:29:42 -0500
| Newsgroups | gmane.comp.lang.ruby.general |
|---|---|
| Message-ID | <CAH7vphS_R_Pg4UAVm61Ye01r3ChmfodK0fePf9Dk5gXi7M8p+Q@mail.gmail.com> |
Do you have a real world example of a code that needs this pattern? The problem with generalized or anonymized examples is they lack the context that would make solving the problem clearer. I would suggest making a minimal example that is tested that you can share with valid input and what output you expect it to return. I suspect that you're conflating a lot of ideas here but can't really understand the code from the examples. I'll have to reread the code a few times to understand what you're doing here, but my initial impression is to break it apart into simple-use functions that do one and only one thing, and compose those in turn. On Sun, Jun 30, 2019 at 11:13 PM John Carter <[email protected]> wrote: > So I'm just thinking out aloud here. > > 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 two new patterns.... > > * Passing enumerators as parameters so I can pull out inner loops as > standalone functions without calling the entire function every time.. > > * Creating functions that return lambdas, so I can pass an inner loop > in as a parameter. > > 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. > > (In the following, a line of ===== indicates the next, slightly > different version of the code) > > ========================================== > > 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 which 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. > > Comments, suggestions, nitpicks welcome.... > > As I said, I'm just thinking aloud trying to fix a common anti-pattern > in my own code. > > -- > John Carter > Tait Electronics > PO Box 1645 Christchurch > New Zealand > > -- > This Communication is Confidential. We only send and receive email on the > > basis of the terms set out at www.taitradio.com/email_disclaimer > <http://www.taitradio.com/email_disclaimer> > > Unsubscribe: <mailto:[email protected]?subject=unsubscribe> > <http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk> > Unsubscribe: <mailto:[email protected]?subject=unsubscribe> <http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk>