Reputation: 2515
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
Upvotes: 1
Views: 69
Reputation: 857
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
Upvotes: 1
Reputation: 84343
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.
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
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]
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]
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]
Upvotes: 3