About Blocks: Variables and Blocks

This is the second post of the block closure series. This blog post is extracted from “Deep Into Pharo” and a new book under writing that will revisit chosen pieces of Pharo. We will start to play with Pharo to illustrate block and their interplay with variables.

Variables and blocks

A block can have its own temporary variables. Such variables are initialized during each block execution and are local to the block. We will see later how such variables are kept. Now the question we want to make clear is what is happening when a block refers to other (non-local) variables. A block will close over the external variables it uses. It means that even if the block is executed later in an environment that does not lexically contain the variables used by a block, the block will still have access to the variables during its execution. Later, we will present how local variables are implemented and stored using contexts.

In Pharo, private variables (such as self, instance variables, method temporaries and arguments) are lexically scoped: an expression in a method can access to the variables visible from that method, but the same expression put in another method or class cannot access the same variables because they are not in the scope of the expression (i.e., visible from the expression).

At runtime, the variables that a block can access, are bound (get a value associated to them) in ”the context” in which the block that contains them is ”defined”, rather than the context in which the block is executed. It means that a block, when executed somewhere else can access variables that were in its scope (visible to the block) when the block was ”created”. Traditionally, the context in which a block is defined is named the ”block home context”.

The block home context represents a particular point of execution (since this is a program execution that created the block in the first place), therefore this notion of block home context is represented by an object that represents program execution: a context object in Pharo. In essence, a context (called stack frame or activation record in other languages) represents information about the current execution step such as the context from which the current one is executed, the next byte code to be executed, and temporary variable values. A context is a Pharo execution stack element. This is important and we will come back later to this concept.

A block is created inside a context (an object that represents a point in the execution).

Some little experiments

Let’s experiment a bit to understand how variables are bound in a block.

Define a class named  BExp  (for BlockExperiment) as a subclass of  TestCase  so that we can define quickly some tests to make sure that our results are always correct.

TestCase subclass: #BExp

instanceVariableNames: ”

classVariableNames: ”

package: ‘BlockExperiment’

 Experiment 1: Variable lookup.

A variable is looked up in the block  definition  context.

We define two methods: one that defines a variable  t  and sets it to 42 and a block  [ t ]  and one that defines a new variable with the same name and executes a block defined elsewhere.

BExp >> setVariableAndDefineBlock

| t |

t := 42.

^ self executeBlock: [ t ]

BExp >> executeBlock: aBlock

| t |

t := 33.

^ aBlock value

BExp new setVariableAndDefineBlock

>>> 42

The following test passes. Executing the  BExp new setVariableAndDefineBlock  expression returns 42 .

BExp >> testSetVariableAndDefineBlock

self assert: self setVariableAndDefineBlock equals: 42

The value of the temporary variable  t  defined in the  setVariableAndDefineBlock  method is the one used rather than the one defined inside the method  executeBlock:  even if the block is executed during the execution of this method.

The variable  t  is  looked up in the context of the block creation (context created during the execution of the method  setVariableAndDefineBlock  and not in the context of the block evaluation (method  executeBlock: ).

Let’s look at it in detail. The following figure shows the execution of the expression  BExp new setVariableAndDefineBlock .

variable.png
  • During the execution of method  setVariableAndDefineBlock , a variable  t  is defined and it is assigned 42. Then a block is created and this block refers to the method activation context – which holds temporary variables.
  • The method  executeBlock:  defines its own local variable  t  with the same name than the one in the block. This is not this variable, however, that is used when the block is executed. While executing the method  executeBlock:  the block is executed, during the execution of the expression  t  the non-local variable  t  is looked up in the  home context  of the block i.e., the method context that  created  the block and not the context of the currently executed method.

Non-local variables are looked up in the  home context  of the block (i.e., the method context that  created  the block) and not the context executing the block.

 Experiment 2: Changing a variable value

Let’s continue our experiments. The method  setVariableAndChangingVariableBlock  shows that a non-local variable value can be changed during the evaluation of a block.

Executing  BExp new setVariableAndChangingVariableBlock  returns 2008, since 2008 is the last value of the variable  t .

BExp >> setVariableAndChangingVariableBlock

| t |

t := 42.

^ self executeBlock: [ t := 2008. t ]

The test verifies this behavior.

BExp >> testSetVariableAndChangingVariableBlock

self assert: self setVariableAndChangingVariableBlock equals: 2008

Experiment 3: Accessing a shared non-local variable.

Different blocks can share a non-local variable and they can modify the value of this variable at different moments.

To see this, let us define a new method  accessingSharedVariables  as follows:

BExp >> accessingSharedVariables

| t |

^ String streamContents: [ :st |

t := 42.

self executeBlock: [ st print: t. st cr. t := 99. st print: t. st cr ].

self executeBlock: [ st print: t. st cr. t := 66. st print: t. st cr ].

self executeBlock: [ st print: t. st cr. ] ]

The following test shows the results:

BExp >> testAccessingSharedVariables

self assert: self accessingSharedVariables equals: ’42

99

99

66

66

BExp new accessingSharedVariables  will print 42, 99, 99, 66 and 66.

Here the two blocks  [ st print: t. st cr. t := 99. st print: t. st cr  ]  and  [ st print: t. st cr. t := 66. st print: t. st cr ]  access the same variable  t  and can modify it. During the first execution of the method  executeBlock:  its current value  42  is printed, then the value is changed and printed. A similar situation occurs with the second call. This example shows that blocks share the location where variables are stored and also that a block does not copy the value of a captured variable. It just refers to the location of the variables and several blocks can refer to the same location.

Experiment 4: Variable lookup is done at execution time.

The following example shows that the value of the variable is looked up at runtime and not copied during the block creation. First add the instance variable  block  to the class  BExp .

Object subclass: #BExp

instanceVariableNames: ‘block’

classVariableNames:

package: ‘BlockExperiment’

Here the initial value of the variable  t  is 42. The block is created and stored into the instance variable  block but the value to  t  is changed to 69 before the block is executed. And this is the last value (69) that is effectively printed because it is looked up at execution-time in the home context of the block. Executing  BExp new variableLookupIsDoneAtExecution  return  ’69’  as confirmed by the tests.

BExp >> variableLookupIsDoneAtExecution

^ String streamContents: [ :st |

| t |

t := 42.

block := [ st print: t ].

t := 69.

self executeBlock: block ]

BExp >> testVariableLookupIsDoneAtExecution

self assert: self variableLookupIsDoneAtExecution equals: ’69’

Experiment 5: For method arguments

We can naturally expect that method arguments referred by a block are bound in the context of the defining method.

Let’s illustrate this point. Define the following methods.

BExp >> testArg5: arg

^ String streamContents: [ :st |

block := [ st << arg ].

self executeBlockAndIgnoreArgument: ‘zork’]

BExp >> executeBlockAndIgnoreArgument: arg

^ block value

BExp >> testArg5

self assert: (self testArg5: ‘foo’) equals: ‘foo’

Executing  BExp new testArg: ‘foo’  returns  ‘foo’  even if in the method  executeBlockAndIgnoreArgument:  the temporary  arg  is redefined.

The block execution looks for the value of  arg  is its definition context which is the one where  testArg5:  ‘  arg  is bound to ‘foo’ due to the execution of method  testArg5 .

 Experiment 6: self binding

Now we may wonder if  self  is also captured in a block. It should be but let us test it.

To test we need another class.

Let’s simply define a new class and a couple of methods.

Add the instance variable  x  to the class  BExp  and define the  initialize  method as follows:

Object subclass: #BExp

instanceVariableNames: ‘block x’

classVariableNames:

package: ‘BlockExperiment’

BExp >> initialize

x := 123.

Define another class named  BExp2 .

Object subclass: #BExp2

instanceVariableNames: ‘x’

classVariableNames:  ”

package: ‘BlockExperiment’

BExp2 >> initialize

x := 69.

BExp2 >> executeBlockInAnotherInstance6: aBlock

^ aBlock value

Then define the methods that will invoke methods defined in  BExp2 .

BExp >> selfIsCapturedToo

^ String streamContents: [ :st |

self executeBlockInAnotherInstance6: [ st print: self ; print: x ] ]

BExp >> executeBlockInAnotherInstance6: aBlock

^ NBexp2 new executeBlockInAnotherInstance6: aBlock

Finally we write a test showing that this is indeed the instance of  BExp  that is bound to  self  even if it is executed in  BExp2.

BExp >> testSelfIsCapturedToo

self assert: self selfIsCapturedToo equals: ‘NBexp>>#testSelfIsCapturedToo123’

Now when we execute  BExp new selfIsCapturedToo , we get  NBexp>>#testSelfIsCapturedToo  printed showing that a block captures  self  too, since an instance of  BExp2  executed the block but the printed object ( self ) is the original  BExp  instance that was accessible at the block creation time. The printing is  NBexp>>#testSelfIsCapturedToo  because  BExp  is  subclass of  TestCase .

Conclusion

We show that blocks capture variables that are reached from the  context in which the block was defined  and not where there are executed. Blocks keep references to variable locations that can be shared between multiple blocks. In subsequent posts we will show how this is optimized by the Pharo compiler and VM to avoid expensive lookups.

Published by Stéphane Ducasse

I'm one of the leader of the Pharo project, co-founder of Synectique, author of several books and far too many other things. http://stephane.ducasse.free.fr I wish you fun and business with Pharo

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: