1

How can I use a variable's value at the point of Proc definition instead of defining the Proc with a reference to the variable? Or how else would I approach the problem of defining a list of different steps to be executed in sequence based on an input sequence?

Example:

arr = []
results = [1,2,3]
for res in results
  arr << Proc.new { |_| res }
end
p arr[0].call(42)
p arr[1].call(3.14)

Expected output:

1
2

Actual output:

3
3
Johannes Riecken
  • 2,301
  • 16
  • 17
  • This is a good question because it exposes some interactions between Ruby keywords and scope gates that are non-obvious. I provide a deeper explanation below. – Todd A. Jacobs Jul 04 '20 at 16:50

2 Answers2

3

Why Your Code Doesn't Work as Expected: Shared Closure Scope

By definition, a Proc is a closure that retains its original scope but defers execution until called. Your non-idiomatic code obscures several subtle bugs, including the fact that the for-in control expression doesn't create a scope gate that provides the right context for your closures. All three of your Proc objects share the same scope, where the final assignment to the res variable is 3. As a result of their shared scope, you are correctly getting the same return value when calling any of the Procs stored in your array.

Fixing Your Closures

You can make your code work with some minor changes. For example:

arr = []
results = [1,2,3]
results.map do |res|
  arr << Proc.new { |_| res }
end

p arr[0].call(42)   #=> 1
p arr[1].call(3.14) #=> 2

Potential Refactorings

A More Idiomatic Approach

In addition to creating a proper scope gate, a more idiomatic refactoring might look like this:

results = [1, 2, 3]

arr = []
results.map { |i| arr << proc { i } }
 
arr.map { |proc_obj| proc_obj.call }
#=> [1, 2, 3]

Additional Refinements

A further refactoring could simplify the example code even further, especially if you don't need to store your inputs in an intermediate or explanatory variable like results. Consider:

array = [1, 2, 3].map { |i| proc { i } }
array.map &:call
#=> [1, 2, 3]

Validating the Refactoring

Because a Proc doesn't care about arity, this general approach also works when Proc#call is passed arbitrary arguments:

[42, 3.14, "a", nil].map { |v| arr[0].call(v) }
#=> [1, 1, 1, 1]

[42, 3.14, "a", nil].map { |v| arr[1].call(v) }
#=> [2, 2, 2, 2]
Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199
  • 1
    More idiomatic still would be `arr = results.map { |i| proc { i } }` and `arr.map(&:call)`. – mu is too short Jul 04 '20 at 17:24
  • You write that a Proc retains its original scope, but isn't the block passed to `map` (or any block argument) also a Proc? Could the difference in scope creation be the reason why Rubocop suggests using `each` instead of for-in loops? – Johannes Riecken Jul 04 '20 at 20:37
  • 1
    @rubystallion There are exceptions to every rule, but the Ruby Style Guide explains why you [generally want to avoid for-loops](https://rubystyle.guide/#no-for-loops). RuboCop more or less implements the style guide, so that's why it recommends using #each and #map to create scope gates that avoid problems like the one you experienced. – Todd A. Jacobs Jul 04 '20 at 21:57
1

The problem is that the proc object use the context inside the loop the following should work

def proc_from_collection(collection)
  procs = []
  collection.each { |item| procs << Proc.new { |_| item } }
  procs
end

results = [1,2,3]

arr = proc_from_collection(results)
p arr[0].call # -> 1

p arr[1].call # -> 2

After reading Todd A. Jacobs answer I felt like I was missing something.

Reading some post on stackoverflow about the for loop in ruby made me realize that we do not need a method here.

We can iterate the array using a method that does not pollute the global environment with unnecessary variables like the for loop does.

I suggest using a method whenever you need a proper closure that behaves according to a Lexical Scope (The body of a function is evaluated in the environment where the function is defined, not the environment where the function is called.).

My first answer is still a good first approach but as pointed by Todd A. Jacobs a 'better' way to iterate the array could be enough in this case

arr = []
results = [1,2,3]
results.each { |item| arr << Proc.new { |_| item } }

p arr[0].call # -> 1

p arr[1].call # -> 2