Understanding Rebol Series

Chris Ross-Gill
7 min readDec 8, 2015

--

Most languages have an abstraction for a group of values. In Rebol, there are blocks (and parens).

Blocks are somewhat analagous to the array type in other languages in that they contain many values and are considered a single value regardless of the number of items contained. Unlike other languages, blocks are not evaluated until an explicit instruction to do so, therefore:

>> [a + 1]
== [a + 1]

There are several functions within Rebol that will evaluate the content of blocks:

do [read http://rebol.com]
if true [print “Foo”]
reduce [zero pi rebol.com true]
try [something bad]

Parens are similar to blocks except that they are evaluated when the interpreter encounters them:

>> (1 + 2)
== 3

However, they are not evaluated if their container is not evaluated:

>> [(1 + 2)]
== [(1 + 2)]

Evaluation is only part of a block’s utility, however. In this article, I’ll look at how they can be manipulated.

The Series Supertype

In Rebol, there are many datatypes that share certain attributes common to series:

  • They contain X number of constituents where X is zero to many,
  • Their content can be iterated,
  • Their content can be accessed numerically,
  • They can be reviewed and modified by a collection of series operators.

There are two series subtypes: blocks and strings — blocks consist of values and strings of characters. We’ll concentrate on blocks for now to explore the series operators.

Series Operators

Let’s start with an empty block:

>> block: []
== []

What do we know about this block? Well, how many items does it contain?

>> length? block
== 0

That was fairly evident, I’d hope.

Position

When referencing a series in Rebol, we not only reference a series, but reference it at a certain position. By default, this would be at the beginning:

>> index? block
== 1

Note: Rebol currently uses one-based indexing — this has been controversial and has many implications when comparing Rebol code to that of other languages.

Series Index/Pick Model

Once we start to add values to our block, we can start to see things differently. The INSERT function adds a value to a series at the current position and returns the position after the inserted value:

>> block-after-insert: insert block 1234
== []

This can be a little disconcerting as it appears that nothing has happened. Let’s review our values though:

>> block
== [1234]
>> index? block
== 1
>> index? block-after-insert
== 2
>> equal? block block-after-insert
== false

It’s clear then that something has happened, our block BLOCK indeed contains a new value. How do we understand BLOCK-AFTER-INSERT then?

BLOCK and BLOCK-AFTER-INSERT both reference the same block, but at different positions. At our disposal are a few functions that allow us to change positions: NEXT, BACK and SKIP (amongst others):

>> back block-after-insert
== [1234]
>> index? back block-after-insert
== 1
>> equal? block back block-after-insert
== true
>> next block
== []
>> index? next block
== 2
>> equal? skip block 1 block-after-insert
== true
>> equal? block skip block-after-insert -1
== true

With these three functions, we can traverse a series based on our current position.

It’s also worth noting that LENGTH? is relative to the current position:

>> length? block
== 1
>> length? block-after-insert
== 0

Heads or Tails

If the preceding functions can step through a series relative to the position within the series, then HEAD and TAIL will take you respectively and absolutely to the beginning or current end of a series:

>> block: [one two three four]
== [one two three four]
>> block-tail: tail block
== []
>> block-head: head block-tail
== [one two three four]

Again, even though BLOCK-TAIL appears to be empty, it references the same block as BLOCK, just at a different position. One the one hand, the appearance of being empty is misleading, on the other, some functions don’t differentiate between an empty block and a block at it’s tail. As an example, iterators such as FOREACH only operate on a block from its current position forward.

>> foreach item next [1 2 3] [probe item]
2
3

We can combine operators to get to a specific position — in this case, last but one:

>> back tail block
== [four]

In addition to the functions that take you to the head or tail of a series, there are functions to test whether we are at the head or tail:

>> head? block
== true
>> tail? block
== false
>> head? block-head
== true
>> tail? block-tail
== true

It’s important to note though that our reference to a series at a position is numeric and that a change in the series prior to the reference does not move the reference accordingly:

>> index? block-tail
== 5
>> insert block 'zero
== [one two three four]
>> block
== [zero one two three four]
>> index? block-tail
== 5
>> block-tail
== [four]
>> tail? block-tail
== false

Take your Pick

To recover values from a series, there is the PICK function. PICK takes a numerical argument and recovers the appropriate item relative to the current position. PICK is also one-based:

>> block: skip [one two three four] 2
== [three four]
>> pick block 1
== three
>> pick block 2
== four

Picking from beyond the tail of a series returns none:

>> pick block 10
== none

Negative values will work where the series is not at the head.

>> pick block -1
== one
>> pick block 0
== two

IMPORTANT NOTE: This behaviour is different in Rebol 2:

>> pick block -1
== two
>> pick block 0
== none

Don’t Try to Change Me!

To alter the content of a series, we can use the INSERT, CHANGE, POKE and REMOVE functions.

As mentioned before, INSERT adds a value to the series at the current position and returns the position after the inserted value:

>> colors: [red green]
== [red green]
>> insert next colors 'orange
== [green]
>> colors
== [red orange green]

To add a value at the tail of a series, APPEND is shorthand for HEAD INSERT TAIL:

>> append colors 'blue
== [red orange green blue]

Ordinarily if you try to insert a block into a block, it will insert the contents of the inserted block:

>> insert tail colors [indigo violet]
== []
>> colors
== [red orange green blue indigo violet]

To insert a block, the /ONLY refinement should be used:

>> insert/only colors [black white]
== [red orange green blue indigo violet]
>> colors
== [[black white] red orange green blue indigo violet]

To modify a value, we use CHANGE. CHANGE replaces the value at the current position with a new one and returns the series position after the changed value:

>> change colors 'silver
== [red orange green blue indigo violet]
>> colors
== [silver red orange green blue indigo violet]

To replace multiple values, using a block as a parameter will change the next X values at the current position where X is the length of the block containing new values:

>> change colors [gold scarlet amber]
== [green blue indigo violet]
>> colors
== [gold scarlet amber green blue indigo violet]

To replace a certain number of values regardless of the length of the block containing new values, we have a /PART refinement:

>> change/part colors [infrared red orange yellow cyan] 3
== [green blue indigo violet]
>> colors
== [infrared red orange yellow cyan green blue indigo violet]

To replace the next value with a single block value, there is an /ONLY refinement similar to that of INSERT:

>> change/only colors [black white]
== [red orange yellow cyan green blue indigo violet]
>> colors
== [[black white] red orange yellow cyan green blue indigo violet]

To replace a single value at a given offset, we have the POKE function. POKE returns the inserted value.

>> poke colors 1 'infrared
== infrared
>> colors
== [infrared red orange yellow cyan green blue indigo violet]

Finally, to remove items from a series, we have the REMOVE function:

>> remove colors
== [red orange yellow cyan green blue indigo violet]
>> remove skip colors 3
== [green blue indigo violet]
>> colors
== [red orange yellow green blue indigo violet]

To remove multiple values, REMOVE has a /PART refinement:

>> remove/part next colors 3
== [blue indigo violet]
>> colors
== [red blue indigo violet]

CLEAR can be used to remove all values from the current position to the tail:

>> clear next colors
== []
>> colors
== [red]

When used in conjunction with HEAD, you can clear out the whole series:

>> clear head insert [] 'red
== []

TAKE will remove the first value and return that value:

>> take colors
== red
>> colors
== []

Manipulating Evaluation

Using just these basic accessors, it is possible to manipulate code on the fly:

>> code: []
== []
>> append code "Foo"
== [“Foo”]
>> insert code 'print
== ["Foo"]
>> probe code
== [print "Foo"]
>> do code
Foo
>> if true code
Foo

This is certainly a feature that makes Rebol stand out: everywhere a block is used—even in evaluation—that block can be manipulated prior to use.

Strings

Strings are somewhat beyond the scope of this article, save to say that strings are also series and many of the series operators work on strings as well:

>> next bar: "bar"
== "ar"
>> remove next bar
== "r"
>> probe bar
== "br"

--

--