This blog post is a first and short of a series where we will explore block-closures. This post will cover basic elements while the other will start to go over more semantical aspects using simple examples. This blog post is extracted from “Deep Into Pharo” and a new book under writing that will revisit chosen pieces of Pharo.
Lexically-scoped block closures, blocks in short, are a powerful and essential feature of Pharo. Without them it would be difficult to have such a small and compact syntax.
The use of blocks is key to get conditionals and loops as library messages and not hardcoded in the language syntax. This is why we can say that blocks work extremely well with the message passing syntax of Pharo.
What is a block?
A block is a lambda expression that captures (or closes over) its environment at creation-time. We will see later what it means exactly. For now, imagine a block as an anonymous function or method. A block is a piece of code whose execution is frozen and can be kicked in using messages. Blocks are defined by square brackets.
If you execute and print the result of the following code, you will not get 3, but a block.
Indeed, you did not ask for the block value, but just for the block itself, and you got it.
[ 1 + 2 ] >>> [ 1 + 2 ]
A block is executed by sending the message value to it.
More precisely, blocks can be executed using value (when no argument is mandatory), value: (when the block requires one argument), value:value: (for two arguments), value:value:value: (for three) and valueWithArguments: anArray (for more arguments).
These messages are the basic and historical API for block execution.
[ 1 + 2 ] value 3 [ :y | y + 2 ] value: 5 7
Some handy extensions
Beyond the value messages, Pharo includes some handy messages
such as cull: and friends to support the execution of blocks even
in the presence of more values than necessary.
cull: will raise an error if the receiver requires more arguments than provided.
The valueWithPossibleArgs: message is similar to cull: but takes
an array of parameters to pass to a block as argument.
If the block requires more arguments than provided, valueWithPossibleArgs:
will fill them with nil .
[ 1 + 2 ] cull: 5 >>> 3 [ 1 + 2 ] cull: 5 cull: 6 >>> 3 [ :a | 2 + a ] cull: 5 >>> 7 [ :a | 2 + a ] cull: 5 cull: 3 >>> 7 [ :a :b | 1 + a + b ] cull: 5 cull: 2 >>> 8 [ :a :b | 1 + a + b ] cull: 5 >>> error because the block needs 2 arguments. [ :a :b | 1 + a + b ] valueWithPossibleArgs: #(5) >>> error because 'y' is nil and '+' does not accept nil as a parameter.
Some messages are useful to profile execution:
- bench. Return how many times the receiver block can be executed in 5 seconds.
- durationToRun. Answer the duration (instance of class Duration ) taken to execute the receiver block.
- timeToRun. Answer the number of milliseconds taken to execute this block.
Some messages are related to error handling – we will cover them when we will present exceptions.
- ensure: terminationBlock. Execute the termination block after evaluating the receiver, regardless of whether the receiver’s evaluation completes.
- ifCurtailed: onErrorBlock. Execute the receiver, and, if the evaluation does not complete, execute the error block. If evaluation of the receiver finishes normally, the error block is not executed.
- on: exception do: catchBlock. Execute the receiver. If an exception exception is raised, execute the catch block.
- on: exception fork: catchBlock. Execute the receiver. If an exception exception is raised, fork a new process, which will handle the error. The original process will continue running as if the receiver evaluation finished and answered nil, an expression like: [ self error: ‘some error’] on: Error fork: [:ex | 123 ] will always answer nil to the original process. The context stack, starting from the context which sent this message to the receiver and up to the top of the stack will be transferred to the forked process, with the catch block on top. Eventually, the catch block will be executed in the forked process.
Some messages are related to process scheduling. We list the most important ones. Since this Chapter is not about concurrent programming in Pharo, we will not go deep into them but you can read the book on Concurrent Programming in Pharo available at http://books.pharo.org.
- fork. Create and schedule a Process evaluating the receiver.
- forkAt: aPriority. Create and schedule a Process evaluating the receiver at the given priority. Answer the newly created process.
- newProcess. Answer a Process evaluating the receiver. The process is not scheduled.
In the next post we will present some little experiences to show how blocks captures variables.