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]