Rebol 1.0.2 User's Guide

Chapter 5: REBOL Context

About Context

Words can be used to represent values. In your script you could define the word two:

two: 2

and then later make use of it:

>> print two
2

>> print 30 + two
32

At any point during definition the set of all defined words forms the current context. In the above example, the context would include not only the word two, but the words print, +, and all other words that were previously defined by the script or by REBOL (such as its native functions). All of these words have values at this point of evaluation, and even though you are not using them all, they are part of the context because you could use them.

If the word two is used outside this context, for instance in another script or another part of the same script, it could have a different value. The context determines its meaning (its value). Because of this it is crucial for you to know the rules that establish the context of words.

In REBOL, the primary rules of context are:

  1. Every script begins with a predefined context which includes REBOL words for native functions (such as print) and system values.

  2. As your script executes, it may define new words in the context.

  3. At specific points functions, and objects may modify the context.

It is this last rule that is our next subject.

Using Context

In a REBOL script, you define various words with specific values. There will be times when you want to use a word that could already be defined elsewhere in your script. If you give that word a new value, you will destroy its previous value. For instance, in your main script the word who could represent a person, such as:

>> who: "Cindy"
>> print who
Cindy

But what if later in your script you want to use the word who again to temporarily refer to the name of a company? You may not want to lose the name of the person, but just "reuse" the word for a short time.

Whenever you begin a new block in REBOL, you can specify what words you want to temporarily redefine within that block. To do this you put the words you want to redefine in a block.

>> use [[who]
       who: "REBOL HQ"
       print who
   ]
REBOL HQ

>> print who
Cindy

The use function tells REBOL that who is to be used temporarily within the block, and you don't want to affect the value of who outside the block. It is said to be local to the block. Any number of words can be specified as local words in this manner.

You have changed the context. The word who used prior to the block now has a different meaning in the new block. This ability to change context can be quite handy, and it is applied to functions and objects as well.

Here is an example of how locals can be used in a block context. It will help illustrate a few points. In it a database of contacts is printed in a specified text format:

; A simple database:
>> persons: [
       "John Able" CEO "Digitalla" ja@dig2.com
       "Jane Baker" VP "Netescape" jane@us.gov
       "Mari Conners" COO "Micoroni" maric@mico.com
   ]

; The fields of the database as words:
>> facts: [name title company email]

; The format used to print the info:
>> text: [name "at" company "is at" email]

; The loop which prints the database:
>> use facts [forskip persons 4 [
       set facts persons
       print text
   ]]
John Able of Digitalla is at ja@dig2.com
Jane Baker of Netescape is at jane@us.gov 
Mari Conners of Micoroni is at maric@mico.com

Notice that within the forskip block the facts word is used to specify the locals. This is valid because facts is a block. Note that the same facts block is used with set which sets each of the words of the block to the values from the current position in the persons database.

Functions

The concept of a function comes from expanding on the block context. You only need two additional concepts:

  1. Functions can be evaluated without requiring do (a function's natural value is to evaluate),

  2. The initial values of local words can be supplied to the function each time it is evaluated.

For example, you might create a simple function plen to print the length of a series:

plen: make function! [series]
[print length? series]

and then use it in multiple places in your script:

>> plen "now is the time"
15

>> plen [1 2 3 4]
4

The value which follows the function is an argument. In the example, both a string argument and a block argument are used. This argument corresponds to the word series that was given in the make function! line above.

Let's take a closer look at the definition of plen. The REBOL native function make was evaluated, and it required three values:

  1. The datatype to be made, in this case a function!

  2. A block of words that will be local to the function's block and used for arguments. These appear in the order that they will appear when the function is used.

  3. The function block to be evaluated.

The new function is returned from make and is set to plen. Note that defining plen to be a function is no different than defining it to be an integer, date, or any other value. Once it has been defined, it can then be used as in the example above.

A function can also define other local words which are not used as arguments. These are placed at the head of the function's evaluation block using the use function. In the example below, name and date are local to the function, but are not arguments.

example: make function! [arg] [
    use [name date][
        ...
]]

A function can also return a value as a result of its evaluation. You can do this from any point in your function with the return native:

find-val: make function! [series value] [
    forall series [
       if (first series) = value [return series]
    ]
]

>> print find-val [1 2 3 4] 3
3 4

If you want to return from a function, but not return a value, use exit instead of return. It is similar to the break function described earlier.

When arguments are supplied to a function, they are evaluated prior to being used in the function. For instance:

>> plen next [1 2 3 4]
3

>> plen skip [1 2 3 4] 3
1

Finally, sometimes you will write a script that refers to a function before it has been defined. REBOL allows this as long as you do not try to evaluate the word before it is defined.

Func Shortcut

A function can accept any type of value as an argument. An argument can be an integer, string, date, block, or any other value, even a function or native. Here is an example in which a function is passed as an argument:

do-it: make function! [func value] [
    func value
]

Here, the word do-it will be used as a function which will accept two arguments and return a result. Because the word do-it is used as a natural value (not written as do-it or :do-it) the natural thing to do is to evaluate it.

>> print do-it :length? "test"
4

>> funcs: [length? first print]
>> print do-it second funcs [1 2 3]
1

The values of the natives passed as arguments required colons. Otherwise they would have been evaluated prior to being passed as arguments, resulting in an error.

A function can return any type of value as a result. You can make a function for creating other functions.

func: make function! [args code] [
    return make function! args code
]

The func word now refers to this function, and when evaluated, it will perform a make function! on the arguments you give it.

>> pall: func [series] [
       forall series [print first series]
   ]

>> pall [1 2 3]
1
2
3

Here, func is given two arguments: a block containing the word series and a block containing the forall. These are used by func as the args and code of the function to make.

As with everything else in REBOL, the arguments to make can be anything that evaluates to a block.

Defining Functions

New functions are created by supplying make with the prototypical function datatype (function!), a block of argument words, and the function body block. The form is:

make function! arguments body

For instance, to create a function to add two values you would write:

make function! [a b] [a + b]

The make returns the new function as a value, and it is often set to a word for later use:

sum: make function! [a b] [a + b]

This new function could then be applied to a set of arguments, as in:

>> print sum 10 2
12

Note that naming a function is completely optional. You may want to do something else with the function value, such as return it as a result from another function. As an example, take the standard REBOL helper function func which is defined as:

func: make function! [args body] [
    make function! args body
]

The first make creates a function which is used for creating functions. Its arguments are the new function's argument and body blocks, and it returns the newly created function as a result. Simple. Powerful. Useful. With it, the sum function above could be written more concisely as:

sum: func [a b] [a + b]

Returning Values

Similar to a block, a function can return its last value as a result. As shown above, this value can be any datatype, including a function value.

Should you want to return from a function at some other point within it, you can do so with return. It accepts one argument — the value to return.

sum-check: func [a b] [
    if a < 0 [return b]
    if b < 0 [return a]
    a + b
]

If you do not wish to return a result from a function, use exit.

Objects

An object is a group of words that have predefined values. An object can be referred to as a single value which can be stored in a variable, passed to functions as an argument, or returned from a function as a result.

Defining Objects

New objects are created with make, the prototypical object datatype (object!), and a block defining the words of the object. Making an object is as easy as making a block containing word definitions:

normal-text: make object! [
    font: "times"
    size: 12
    color: black
]

The new object is returned from make and can be handled as any other value. In the example above, the object is assigned to the word normal-text, but it could just as easily have been returned from a function.

The block which holds the word definition is evaluated, so it may contain complex definitions of values:

normal-text: make object! [
    font: lookup-font 'normal
    size: standard-size + 2
    color: pick color-set 4
]

For convenience, you can cascade the definition of multiple words:

default-text: make object! [size: color: font: none]

The words of an object can be defined to be any kind of value, including functions and other objects:

account: make object! [
    balance: 0
    deposit: func [amount] [balance: balance + amount]
    withdraw: func [amount] [balance: balance - amount]
]

The benefit of doing this is that the functions will evaluate within the context of the object. This allows you to encapsulate (keep together) the values and functions of the object.

Accessing Object Variables

Once an object has been created, you can access the variables within it by using paths. (In REBOL, a path is a means of refining a reference to a value.) For instance, to obtain the value for the balance in the account object above, you can write:

print account/balance

If you want to deposit or withdraw from the account, you can use its defined functions as:

account/deposit 1000
account/withdraw 20

If so desired, you can even set a word within an object:

account/balance: 2000

In addition, the set and get functions can be used to access the values of an object:

print set account 'balance 1000
do get account 'withdraw 100

These provide a standard function-based approach to access, allowing complex access expressions such as:

do get account (if thrifty ['deposit] else ['withdraw]) 100

Making Instances

Once an object has been created, you can make another instance (a duplicate) of it with the make function:

my-account: make account []

If you want to change the value of a word within the account, you can also do so:

my-account: make account [balance: 400]

Note that the initial values for a new object will be duplicated as well:

>> new-account: make my-account []
>> print new-account/balance
400

However, for more complex objects, create your own make function. This allows you to consistently initialize the object each time.

>> make-account: func [starting-balance] [
       make account [
           balance: starting-balance + (starting-balance / 20)
       ]
   ]

>> my-account: make-account 400
>> print my-account/balance
420

Extending Objects

You can extend the definition of an object to create new types of objects which are based on the original. Simply define new words within the block of the new object. These words will be added to the others that are already part of the object.

bank-account: make account [
    bank-name: "Mendocino Savings"
    owner: none
]

Now you can create a new bank account:

>> an-account: make bank-account [
       owner: "Carl"
       balance: 2000
       display: func [] [print [owner "has" balance]]
   ]

>> an-account/deposit 40
>> an-account/display
Carl has 2040

Hidden Variables

You can hide specified words from direct access by making them hidden variables when you define the object. This is can be done with either a function definition or the use native.

>> make-bolt: func [size] [
       make object! [
           set-size: func [value] [size: value]
           get-size: func [] [size]
       ]
   ]

>> bolt: make-bolt 10
>> bolt/get-size
10

This works because REBOL supports extended context. The use of hidden variables is supported by a REBOL helper function called make-object. For arguments it takes the parent object, a block of hidden variable words, and the object initialization block.

bolt: make-object object! [size] [
    set-size: func [value] [size: value]
    get-size: func [] [size]
]

Extended Context

In REBOL, variables have a quality known as indefinite extent. This allows the life of hidden variables and their context to be extended for as long as they might be needed. This is a powerful feature that makes scripting easier. For instance, in the example above, what would happen if the computation returned a block that contained the hidden variables?

result: use [total longest] [
    total: longest: 0
    foreach distance streets [
        total: total + distance
        if distance > longest [longest: distance]
    ]
    [total longest]
]

The block stored in result refers to the variables total and longest which were local to the block. In many programming languages this would be considered an error. In REBOL, the hidden variables are extended beyond the use block. Accessing their values will produce the correct results.

>> print get first result
278

These extended variables have some useful qualities. For instance, if you wanted to create two functions which share a common variable, you could write:

use [count] [
    count: 0
    count-add: func [num] [count: count + num]
    count-sub: func [num] [count: count - num]
]

>> count-add 10
>> print (count-sub 1)
9

Binding Into Context

Small databases, descriptive content-oriented blocks, and messages sent from other computers may have been created external to the context in which they must be used. As long as all of the words within these blocks are used as symbols, then there will be no problems.

However, you may want to use some of the words as variables. For instance, a user interface description might contain not only the size, color, and text of a button, but also an action block which is to be evaluated when the button is pressed. That block may contain words which only have values within the user-interface handling code. In order to operate correctly, the words must be associated with their correct values in the current context.

The bind native imports a block by binding its words to the current context regardless of where it was first defined. The new block is returned. For instance, a database might contain code that is to be evaluated in the current context:

database: bind operations-database
do select database 'button

As this chapter illustrates, REBOL is a powerful messaging language. Additional REBOL words and specifics of the REBOL language are included in the Expert’s Guide.