Johannes Riecken
Johannes Riecken

Reputation: 2515

Use variable's value in Proc definition

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

Answers (2)

Giuseppe Schembri
Giuseppe Schembri

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

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84343

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]

Upvotes: 3

Related Questions