Birdman
Birdman

Reputation: 1524

How do Ruby multiple code blocks work in conjunction/when chained?

Here's a function in Ruby to find if 2 unique number in an array add up to a sum:

def sum_eq_n? (arr, n)
    return true if arr.empty? && n == 0

    p "first part array:" + String(arr.product(arr).reject { |a,b| a == b })
    puts "\n"

    p "first part bool:" + String(arr.product(arr).reject { |a,b| a == b }.any?)
    puts "\n"

    p "second part:" + String(arr.product(arr).reject { |a,b| a + b == n } )
    puts "\n"

    result = arr.product(arr).reject { |a,b| a == b }.any? { |a,b| a + b == n }
    return result
end

#define inputs
l1 = [1, 2, 3, 4, 5, 5]
n = 10

#run function
print "Result is: " + String(sum_eq_n?(l1, n))

I'm confused how the calculation works to produce result. As you can see I've broken the function down into a few parts to visualize this. I've researched and understand the .reject and the .any? methods individually.

However, I'm still confused on how it fits all together in the 1 liner. How are the 2 blocks evaluated in combination? I've only found examples with .reject with 1 code block afterwards. Is .reject applied to both? I also thought there might be an implicit AND in between the 2 code blocks, but I tried to add a 3rd dummy block and it failed, so at this point I'm just not really sure how it works at all.

Upvotes: 1

Views: 213

Answers (3)

Kache
Kache

Reputation: 16737

You can interpret the expression via these equivalent substitutions:

# orig
arr.product(arr).reject { |a,b| a == b }.any? { |a,b| a + b == n }

# same as
pairs = arr.product(arr)
pairs.reject { |a,b| a == b }.any? { |a,b| a + b == n }

# same as
pairs = arr.product(arr)
different_pairs = pairs.reject { |a,b| a == b }
different_pairs.any? { |a,b| a + b == n }

Each block is an argument for the respective method -- one for reject, and one for any?. They are evaluated in order, and are not combined. The parts that make up the expression can be wrapped in parenthesis to show this:

((arr.product(arr)).reject { |a,b| a == b }).any? { |a,b| a + b == n }

# broken up lines:
(
  (
    arr.product(arr)          # pairs
  ).reject { |a,b| a == b }   # different_pairs
).any? { |a,b| a + b == n }

Blocks in Ruby Are Method Arguments

Blocks in Ruby are first-class syntax structures for passing closures as arguments to methods. If you're more familiar with object-oriented concepts than functional ones, here is an example of an object (kind of) acting as a closure:

class MultiplyPairStrategy
  def perform(a, b)
    a * b
  end
end

def convert_using_strategy(pairs, strategy)
  new_array = []
  for pair in pairs do
    new_array << strategy.perform(*pair)
  end
  new_array
end

pairs = [
  [2, 3],
  [5, 4],
]

multiply_pair = MultiplyPairStrategy.new
convert_using_strategy(pairs, multiply_pair) # => [6, 20]

Which is the same as:

multiply_pair = Proc.new { |a, b| a * b }
pairs.map(&multiply_pair)

Which is the same as the most idiomatic:

pairs.map { |a, b| a * b }

Upvotes: 2

tadman
tadman

Reputation: 211740

Each operation "chains" into the next, which visualized looks like:

arr.product(arr).reject { |a,b| a == b }.any? { |a,b| a + b == n }
|--|------A----->-----------B----------->-------------C----------|

Where part A, calling .product(arr), evaluates to an object. This object has a reject method that's called subsequently, and this object has an any? method that's called in turn. It's a fancy version of a.b.c.d where one call is used to generate an object for a subsequent call.

What's not apparent from that is the fact that product returns an Enumerator, which is an object that can be used to fetch the results, but is not the actual results per-se. It's more like an intent to return the results, and an ability to fetch them in a multitude of ways. These can be chained together to get the desired end product.

As a note this code can be reduced to:

arr.repeated_permutation(2).map(&:sum).include?(n)

Where the repeated_permutation method gives you all 2-digit combinations of numbers without duplicate numbers. This can be easily scaled up to N digits by changing that parameter. include? tests if the target is present.

If you're working with large arrays you may want to slightly optimize this:

arr.repeated_permutation(2).lazy.map(&:sum).include?(n)

Where that will stop on the first match found and avoid further sums. The lazy call has the effect of propagating individual values through to the end of the chain instead of each stage of the chain running to completion before forwarding to the next.

The idea of lazy is one of the interesting things about Enumerable. You can control how the values flow through those chains.

Upvotes: 2

Brent Smith
Brent Smith

Reputation: 132

The return result of the first method is returned and used by the second method.

This:

result = arr.product(arr).reject { |a,b| a == b }.any? { |a,b| a + b == n }

is functionality equivalent to:

results = arr.product(arr).reject { |a,b| a == b} # matrix of array pairs with identical values rejected
result = results.any? { |a,b| a + b == n }  #true/false

This might be best visualized in pry (comments mine)

[1] pry(main)> arr = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
[2] pry(main)> n = 10
=> 10
[3] pry(main)> result_reject = arr.product(arr).reject { |a,b| a == b } # all combinations of array elements, with identical ones removed
=> [[1, 2],
 [1, 3],
 [1, 4],
 [1, 5],
 [1, 5],
 [2, 1],
 [2, 3],
 [2, 4],
 [2, 5],
 [2, 5],
 [3, 1],
 [3, 2],
 [3, 4],
 [3, 5],
 [3, 5],
 [4, 1],
 [4, 2],
 [4, 3],
 [4, 5],
 [4, 5],
 [5, 1],
 [5, 2],
 [5, 3],
 [5, 4],
 [5, 1],
 [5, 2],
 [5, 3],
 [5, 4]]
[4] pry(main)> result_reject.any? { |a,b| a + b == n } # do any of the pairs of elements add together to equal ` n` ?
=> false
[5] pry(main)> arr.product(arr).reject { |a,b| a == b }.any? { |a,b| a + b == n }  # the one liner
=> false

Upvotes: 2

Related Questions