Rebol Programming/Programming in Rebol

Programming in Rebol edit

Functional and symbolic programming edit

A lot of the power of Rebol comes from the fact that it is both a functional programming language and a symbolic language.

As a functional programming language Rebol uses a sequence of expressions (built from functions) that are evaluated to produce a flow of results which pass from expression to expression. Rebol has no keywords, and words evaluate depending upon context. Generally the order of evaluation is from left to right with some exceptions for special operators.

A symbolic language is one that lets you represent and manipulate symbols (words) the same as any other values of the language. The advantages of symbolic programming will become more clear as your Rebol skills improve. It is the key that unlocks the door to dialecting, allowing you create even more powerful programs with less code, as well as sending Rebol expressions around the Internet to be evaluated on other computer systems, not just your own. (Called "distributed computing".)

Delimiters edit

Whitespace characters, such as space, tab, newline, newpage act as delimiters as in English, separating values from one another. They signal the end of one value and the start of another one.

As an example, this is a series of three values (integer 1, operator + and integer 2), which represent an expression:

>> 1 + 2

Notice the spaces. If we omit the spaces, we obtain:

>> 1+2

which is considered as one, syntactically illegal value.

Other characters that can take a delimiter role are: ( ) " [ ] { } ;.

This is important as when writing Rebol code, the language allows freedom of choice so that your program can be written entirely on one rather long line, or split and indented on multiple lines. The latter is the preferred method, and the reader is advised to follow the published guidelines on how to format source code. (Rebol/Core User's Guide - Section 5 - Style Guide)

Operators and simple expressions edit

Rebol uses arithmetic operators: +, -, *, /, //, **, boolean operators: and, or, xor and comparison operators: <, <=, <>, =, ==, =?, >, >=. They usually are binary, consuming an argument on each side. Example:

>> 2 > 3
== false

Every operator has got a function counterpart:

Operator Function
+ add
- subtract
* multiply
/ divide
// remainder
** power
and and~
or or~
xor xor~
< lesser?
<= lesser-or-equal?
<> not-equal?
= equal?
== strict-equal?
=? same?
> greater?
>= greater-or-equal?

For example, the above expression can be rewritten as follows:

>> greater? 2 3
== false

Exceptions:

  1. Operators can be used as prefix (although it generally isn't advisable, because it could damage the readability of the code, and, moreover, it isn't officially supported).
  2. The - operator can be used as unary (the function counterpart is called negate).

Examples:

>> + 2 3
== 5
>> - 2
== -2

Complex operator expressions edit

More complex operator expressions can be written. There are two rules to keep in mind:

  1. All operators have the same precedence.
  2. Operator expressions are evaluated from left to right.

Example:

>> 1 + 2 * 3
== 9

Notice that the addition on the left was performed before the multiplication. If we wanted to perform the multiplication first, we could have reordered the expression:

>> 3 * 2 + 1
== 7

Or use parentheses (always use parentheses with long formulas, you'll avoid mistakes) :

>> 1 + (2 * 3)
== 7

Let us suppose that we wish to compare the results of two expressions: 1 + 3 and 2 + 2. We should use parentheses to achieve the desired evaluation order:

>> (1 + 3) = (2 + 2)
== true

While the first pair of parentheses is not necessary (the leftmost addition is performed first anyway), that is not true for the rightmost addition. If we omitted the second pair of parentheses, we would compare the result of the leftmost addition with 2, which is legal, but we would obtain an illegal addition trying to add 2 to false:

>> 1 + 3 = 2 + 2
** Script Error: Expected one of: logic! - not: integer!
** Near: 1 + 3 = 2

The error report doesn't look very understandable, it is the same report we would have obtained in the following case:

>> false + 2
** Script Error: Expected one of: logic! - not: integer!
** Near: false + 2

Collecting expression results edit

Usually we evaluate only one expression and need one result. However, it is possible to evaluate more expressions one after another:

>> 4 + 6 7 + 8 
== 15

It evaluated 4 + 6, then 7 + 8 and showed just the last one. A similar example can be written using parentheses:

>> (4 + 6) (7 + 8)
== 15

Rebol displays only the last expression, yet keeps track of all expressions.

>> do [a: 4 + 6 7 + 8]
== 15
>> a
== 10

that is the same of:

>> do [
 a: 4 + 6
 7 + 8
 ]
== 15

To get all results from two or more expressions, we can use a reduce function as follows:

>> reduce [4 + 6 7 + 8]
== [10 15]

We obtained a block containing all the collected results.

Another important case when we may need to collect some results may be the evaluation of a function, which expects some arguments. Let's take the add function as an example:

>> add 2 + 3 4 + 6
== 15

Explanation: in this case we asked the interpreter to evaluate the add function. The interpreter had to collect two arguments for the function, so it evaluated two expressions to the right and obtained two results, which could be used as arguments. It's the same of:

>> add (2 + 3) (4 + 6)
== 15


The above description is valid for functions with more or less arguments, i.e. particularly for functions taking just one argument too:

>> abs -4 + -5
== 9

Here the interpreter needed to evaluate the abs function. Therefore it evaluated the first expression to the right and obtained -9, which it used as the argument for the abs function. It's the same of:

>> abs (-4 + -5)
== 9

So use parentheses to avoid mistakes...

Because of the way how the interpreter collects the arguments for functions, it looks as if the operator expression to the right took precedence over the function evaluation.

It is easy to explain, why this is not the case when the function uses unevaluated (resp. fetched) arguments: in that case the interpreter doesn't need to evaluate an expression to obtain the value of the argument.

Simple programming in the console edit

The console is your primary location for writing small segments of code to rapidly test out your ideas and functions. Nothing needs to be compiled and expressions are evaluated as soon as you press Enter. This allows for interactive programming at the console, and aids in debugging large programs. The lack of a compilation phase allows for rapid prototyping and testing.

We saw a math example earlier, but now let's try something more complex:

>> join "Hi" "There"
== "HiThere"

The join function combines two strings and returns a new string: "HiThere".

Now let's reverse all the characters in that string by inserting reverse in front of join:

>> reverse join "Hi" "There"
== "erehTiH"

We can reverse it again:

>> reverse reverse join "Hi" "There"
== "HiThere"

Now you are observing basically how the Rebol language works:

You can manipulate returned values directly as they are output to the left of a function. Values are streaming from the right to the left in your program and you can manipulate them along the way.

This is a very important observation! It's possible to perform such manipulations indefinitely. This is basically how to build programs with Rebol.

>> reverse print join "Hi" "There"
HiThere
** Script Error: reverse expected value argument of type: series tuple pair
** Near: reverse print join "Hi" "There"

What happens is that the print function doesn't return a value that the last reverse function can use. That is why you'll see on the console both the output of print and the error output of reverse.

Another exception to this rule are binary operators. They usually consume an argument on each side.

The outer box is only for illustrative purposes, but this is how you should see the operation.

Essentially:

Functions return values
You can use other operators or functions to their left.
Operators return values
Similar to functions, but consume (usually) arguments on both sides. You can use functions to the left or other operators (usually to the right) of operator expressions to consume their output.

An example mixing functions and operators:

The equivalent code is:

>> print divide 2 + 2 8
0.5

An example where operator * uses the output of operator +:

>> 2 + 3 * 4
== 20

Remembering values: variables edit

As has been stated above, Rebol words can be used as symbols. In addition to that, we can have words "working" as variables, i.e. referring to other Rebol values.

Let's try that by building a multi-line book-keeping program. We want to use a variable called wallet.

At first, let's see what happens when we enter that in the console:

>> wallet
** Script Error: wallet has no value
** Near: wallet

This happened, because the word wasn't assigned a value. We need to assign a value to it:

>> wallet: $25

In my wallet is 25 dollars. I can return this value simply by typing:

>> wallet
== $25.00

Now I want to add 5 dollars:

>> wallet: wallet + $5
== $30.00

Another way to do that is to write:

>> wallet: add wallet 5
== $35.00

It can get tedious to write this all the time, so we want to create a function to handle the task of adding money to our wallet. A function is simply a piece of program code that is executed, every time you type in a specific word. A simple function is created like this:

>> earn: does [wallet: add wallet $5]

It's the same code as shown above, but enclosed in square brackets called a block, fed to the does function, which means create a function doing this code block everytime it is evaluated. It needs to be stored, and that happens the same way as storing numbers. We assign the new function to the word earn.

This is one of the great strengths of Rebol, namely that storing values and functions work the same way! We are still following our right-to-left rule.

This opens up some very clever possibilities, which we'll explore in later chapters.

Our earn function has been created. So every time you type:

>> earn
$40.00

>> earn
$45.00

It's a lot easier to type, right? But what if you want to earn a different amount each time? Like we saw earlier with reverse and join, those functions take one or more arguments. Therefore we'll need to use func instead of does.

>> earn: func [amount] [wallet: add wallet amount]

Note that another block is used before our program code. This is where we store function arguments, also known as an argument list. The number 5 has been replaced in the code by amount, which is a variable from the argument list. You can take as many arguments as you want from the argument list and use them as many times as you want in your function code.

We can equally create a spend function:

>> spend: func [amount] [wallet: subtract wallet amount]

Now you can use them just like any other function:

>> earn $10
== $45.00

>> spend $25
== $20.00

Did you notice that we actually used a real $ sign in the return value for earn and spend? Using it, Rebol recognizes the number as a money amount!

Why Rebol does assignment like that edit

You might now be asking, why does Rebol use the above syntax for variable assignment? After all, most languages use an equal sign like this:

number = 10

Why does Rebol write it this way:

number: 10

It turns out Rebol does not do it just to be different, it is an important part of the language.

As we hinted early, under the hood, Rebol is an advanced language. It's a step beyond most other languages because Rebol integrates the concepts of code, data, and metadata (data that describes other data) into a single language. (We will talk about that later when we cover "dialecting".)

But for now, think of it this way. When you write:

number = 10

You are stating:

variable assignment-operator value

But, when you write:

number: 10

You are stating:

variable-defined-as value

This fact allows Rebol's variable definitions to stand out as special datatypes of the language. The definition is unique and is not dependent on the meaning of an operator (the = sign). This is a powerful concept that is useful when you are dealing with advanced code-as-data and metadata.

If you still have trouble relating to this notation, think of it this way: what is more common in written human languages? In fact, if you look at an email header or an http header, how are value fields expressed? The Rebol way. Why is that?

Multi-line blocks edit

The console allows you to work with multiple lines of code for a single expression. If you begin a block with [ and press Enter, the console will not cease to accept input, until you give the corresponding ].

To let you know the console is now accepting multi-line input, the prompt changes from >> to the delimiter you are currently using, e.g. [, and it will stay that way, until the ] comes.

>> todays-earnings: [
[    $25.00
[    $30.00
[    $14.00
[    $10.00
[    ]

Multi-line strings are also possible, but note that multiline strings use the { } instead of " ".

>> very-long-string: {
{    This string
{    contains many
{    lines.
{    }

Pasting code in the console edit

Sometimes, you might want to try code examples in the console to try out a function. Simply copy it from your source and paste it (in Windows with Ctrl+C and Ctrl+V).

Common pitfalls edit

Literal series edit

The most common mistake made by all those learning Rebol is in dealing with literal series within a function.

For example, here is a function that prints out a "Dear " given a name as an argument.

dear: func [
    name [string!]
    /local salute
][
    salute: "Dear "
    print append salute name
]

When the function is first called, the result is as expected

>> dear "John"
Dear John

However, when run again, the result is unexpected

>> dear "Jane"
Dear JohnJane

This happens because the salute variable is initialised to the literal string "Dear ", which is kept in the body of the function, and becomes a subject to change. The append function alters that string by adding the actual argument string to it.

You can verify this by examining the dear function before it is first evaluated:

probe :dear

which results in

func [
    name [string!]
    /local salute
] [
    salute: "Dear "
    print append salute name
]

And then when you examine it after it has been evaluated with the argument "John"

func [
    name [string!]
    /local salute
] [
    salute: "Dear John"
    print append salute name
]

It is now evident that the string has been altered to "Dear John".

To ensure that the salute variable is correctly initialised each time, we need to keep the string "Dear " in the function body unaltered. To protect the string in the function body we can assign just a copy of it to the salute variable as follows:

Dear: func [
    name [string!]
    /local salute
] [
    salute: copy "Dear "
    print append salute name
]

This guarantees, that the changes made to the salute string cannot alter the original string in the function body.

The same pitfall occurs when using a block

>> test: func [l [integer!]] [b: [] repeat x l [append b x]]
>> test 2
== [1 2]
>> test 3
== [1 2 1 2 3]

To make sure that the test function does not alter the block it contains, we can use

b: copy []

in the body of the test function in this case.

Precedence edit

Rebol evaluates left to right, but infix operators take precedence over functions upsetting the normal flow of evaluation.

A common error occurs when comparing values

if length? series < 10 [print "less then 10"]
** Script Error: length? expected series argument of type: series port tuple bitset struct
** Near: if length? series < 10

Here the infix operator < takes precedence over the length? function. The result which is a boolean value is then passed to the function length? which is in fact expecting a series argument instead of a boolean.

To circumvent this, the expression can be rewritten as

if (length? series) < 10 [print "less than 10"]

or as

if 10 > length? series [print "less than 10"]

In the latter version, the infix operator takes precedence, and evaluates 10 and then length?. Since length? requires one argument, it consumes series and returns the value to > to give the correct evaluation.

Missing arguments edit

Another common mistake, made by both beginners and experts, is to forget to provide an argument to a function.

In the example below, we forget to provide the last argument to the append function:

>> append "example"
** Script Error: append is missing its value argument
** Near: append "example"

What does the error mean by "value" argument? Use help to find out:

>> help append
USAGE:
   APPEND series value /only
DESCRIPTION:
    Appends a value to the tail of a series and returns the series head.
    APPEND is a function value.
ARGUMENTS:
    series -- (Type: series port)
    value -- (Type: any)
REFINEMENTS:
    /only -- Appends a block value as a block

Here you can see that "value" is the second argument. That's what is missing.

Extra arguments edit

Another common argument mistake is to provide too many arguments. This type of mistake is more subtle, because you won't get an error message.

Here is an example. Let's suppose you have the expression:

if num > 10 [print "greater"]

But, then you decide you want to print "not greater" for the else case. You might be tempted to write:

if num > 10 [print "greater"] [print "not greater"]

This is an error, but when you try it, you won't get an error message. The second block will be ignored. That's because the if function only takes one block. If you want both blocks, you should use the either function as shown here:

either num > 10 [print "greater"] [print "not greater"]

Keep an eye out for these kinds of errors in your code.

Note: There is a very good reason why Rebol allows these "extra" values without producing an error. It is actually one of the special features of Rebol. Consider the reduce function that was mentioned above. Allowing multiple expressions to follow each other allows you to create special data results that can be very useful. This is an advanced topic, but take a look at the example below to get an idea of its importance in Rebol:

>> reduce [if num > 10 [123] ["example" 456]]
== [123 ["example" 456]]

Wrong type of arguments edit

There is also the situation that occurs if you do not provide the proper type of argument to a function. In some specific cases, the error message might be a bit confusing.

Modifying the if example above, what if you wrote:

if num > 10 print "greater"

You would get this error:

** Script Error: if expected then-block argument of type: block
** Near: if num > 10 print

That happened because you did not provide the correct type of argument to the function. What you meant to write was:

if num > 10 [print "greater"]

Again, if you wonder what Rebol means by "then-block", use the help function:

>> help if
USAGE:
   IF condition then-block /else else-block
DESCRIPTION:
    If condition is TRUE, evaluates the block.
    IF is a native value.
ARGUMENTS:
    condition -- (Type: any)
    then-block -- (Type: block)
REFINEMENTS:
    /else -- If not true, evaluate this block
        else-block -- (Type: block)


Copy vs copy/deep edit

Given the following series

a: [a b c [d e f]]

Perform copy

b: copy a
probe b
>> [a b c [d e f]]

Now b contains words a, b, c and its fourth element is the same as the fourth element of a. This can be seen by appending to a/4 and checking b again

append a/4 'g
probe b
>> [a b c [d e f g]]

On copy/deep, c/4 becomes a separate copy of a/4, so the subblocks a/4 and c/4 are not identical.

c: copy/deep a
append a/4 'h
probe c
>> [a b c [d e f g]]

c/4 does not have h but b/4 does.

probe b
>> [a b c [d e f g h]]

Neither b nor c will contain i as it is in the outer series

append a 'i
probe c
>> [a b c [d e f g]]
probe b
>> [a b c [d e f g h]]

Code readability edit

Even in the console, you can produce quite long sequences of functions and variables. If you put it all in one line, it can be hard to read, especially since Rebol makes it unnecessary to using parantheses for evaluation:

print multiply add 4 add 6 5 divide 1 square-root 3

The trained eye may know where to start, but such a mixture of using functions inside arguments can quickly become hard to read.

One method is to split the code into multiple lines. With Rebol you are completely free to do that:

print
  multiply
    add 4
      add 6 5
    divide
      1
      square-root 3

This helps to understand what's going on.

print (4 + 6 + 5) * (1 / square-root 3)

Is an easier equivalent for the same expression.

External references edit