Closures

In this chapter, you will learn about the basics of closures and how you can use them to execute code in different execution contexts.

What is closure?

A closure is an anonymous function that carries its creation context where ever it goes.

Block Objects are Closures

Changing the Value Outside a Block

Let's write a simple program to illustrate what happens to the block object when we change the values of a local variable.

x = 0
seconds = -> { x }
p seconds.call

This prints:

0

Let's change the value of x and print it.

x = 0
seconds = -> { x }
p seconds.call

x = 1
p seconds.call

This prints:

0
1

The value of x is changed to 1 after the block object is created. But, the change is reflected when we execute the code in the block object. This illustrates that the identifier x is actually a reference to Fixnum object.

Reference to 0

If you change that reference to point to a different Fixnum object, it will point to it.

Reference to 1

Changing the Value Inside a Block

Let's write a simple program to illustrate what happens when the value changes inside the block.

x = 0
seconds = -> { x += 1 }
p seconds.call
p seconds.call
p seconds.call
p seconds.call

This prints:

1
2
3
4

The counter increases by one on each call.

Carrying the State Around

The block encapsulates the state. Earlier, we saw that we can pass this Proc object as an argument to a method. What happens when we have another variable with the same name in that method?

x = 0
seconds = -> { x += 1 }

def tester(s)
  x = 100
  p s.call
  p s.call
  p s.call
  p s.call  
end

tester(seconds)

This prints:

1
2
3
4

The variable x with the value 100 inside the tester method did not have any effect on the Proc object. This illustrates an important concept. The block object carries the state found at the point of its creation. In our example, it is this line:

seconds = -> { x += 1 }

At this line, the value of x was 0. It carries this value into the new scope of the tester method. We already know that methods create a new scope and x = 100 is in a new scope. The identifier names are the same but they belong to different execution contexts. One at the top level scope and the other at the method definition scope.

The x in Different Scopes

The block object encapsulates the state. In this case, the value of x. The x gets incremented every time we call the block object by sending the call message. When a piece of code carries its creation context around with it like this, we call it closure.

Insight

You can execute code in a different execution context without using eval by using closures.

Executing Code using Closure

We captured the binding at the top level scope in a block object and executed the code in the block object in the method level scope.

Twin Analogy

Haylee and Kaylee are twins who live together in San Francisco.

Twins

Haylee is going on a business trip to New York.

New York City

She packs a tooth paste that is 100% full in her suitcase. The packing of the suitcase is the creation of the proc object using the literal constructor, ->. The top level context is San Francisco. The InNewYork class represents New York location.

toothpaste_level = 100
p "In SF : #{toothpaste_level}"

brush = -> { toothpaste_level -= 5 }
brush.call

p "After brushing in SF : #{toothpaste_level}"

class InNewYork
  def get_ready(block)
    p "Brushing in NY"
    current_level = block.call
    p "In NY : #{current_level}"
  end
end

InNewYork.new.get_ready(brush)
p "In SF : #{toothpaste_level}"

This prints:

In SF : 100
After brushing in SF : 95
Brushing in NY
In NY : 90
In SF : 90

Toothpaste

When Haylee brushes her teeth in New York, the mirror image of that toothpaste in San Francisco is affected. Kaylee in San Francisco observes the toothpaste usage of her twin in New York. Of course, in reality physical objects cannot be in two locations at the same time. But, that's how closures behave in a programming environment.

Evaluating Code using Binding Object

Execute Code in Top Level Context

The block objects provide a binding method that we can use to execute code. Here is an example to illustrate that concept.

x = 1
o = -> { x }

def tester(b)
  x = 10
  eval('x', b)
end

p tester(o.binding)

This prints 1. Inside the tester method the value of x is 10. But, we execute code in the top level execution context by passing in the binding of the block object. Thus, the value of x is 1 inside the tester method.

Execute Code in Method Level Context

How can we switch the execution context to the method level scope? Here is an example.

x = 1
o = -> { x }

def tester
  x = 10
  eval('x', binding)
end

p tester

This prints 10. The binding call inside the tester method provides the execution context within that method. Thus, the x = 10 initialized value is available in the method level scope.

Execute Code in an Object Context

In the previous chapter, we could not call the private binding method of an object. Here is that example.

class Car
  def initialize(color)
    @color = color
  end

  def drive
    'driving'
  end  
end

car = Car.new('red')
p eval("@color", car.binding)

This gave us the error:

NoMethodError: private method ‘binding’ called for #<Car:0x0570 @color="red">

We can make this example work by accessing the binding within the car object.

class Car
  def initialize(color)
    @color = color
  end

  def drive
    'driving'
  end  

  def context
    binding
  end
end

car = Car.new('red')
p eval("@color", car.context)

This prints red. We can also use the binding of a block object to access the instance variable.

class Car
  def initialize(color)
    @color = color
  end

  def drive
    'driving'
  end  

  def null_proc
    -> { }
  end

end

car = Car.new('red')
p eval("@color", car.null_proc.binding)

This also prints red. This illustrates the concept that the null_proc is a closure. Let's check the class of the null_proc.

p car.null_proc.class

This prints Proc. The null_proc is bound to the execution context that can access the instance variable of the car object. The binding instance method on the Proc class exposes the execution context.

You can also replace the existing null_proc implementation with:

Proc.new{ }

This creates a proc object from an empty block. This will still work.

Summary

In this chapter, you learned the basics of closures. You also learned how to execute code in different contexts using closures.

results matching ""

    No results matching ""