Chapter 3: REBOL Series
About Series
In REBOL, a group of values can form a series . Series are frequently used in computing and REBOL treats them in a consistent manner. For instance, a series could be a:
- set of values
- string of characters
- directory of files
- mailbox of messages
- group of tasks
- database of records (as in a checkbook)
- sequence of images (as in a movie)
- sequence of sounds
- array of pixels (as in an image)
- array of samples (as in a sound)
and more...
The essential characteristics of a series are that it contains a set of values that are organized in a particular order .
A movie consists of a number of still images which are placed in the order that they are to be shown. This sentence contains a series of words which you read in the order that they were written.
The order implies a few simple relationships. Something comes first, something comes next. There is a beginning (head) and an end (tail). You can skip forward or backward in the series. You can count its parts (length) and you can refer to them by their positions (e.g. the fifth word in a sentence).
The following section covers taking advantage of the power of series in REBOL scripts. Refer to the Expert’s Guide for complete descriptions and examples of REBOL series.
Blocks
Both code and data can be grouped into a type of series called a block . Blocks can be directly expressed in REBOL as a set of values enclosed in square brackets:
[10:42 "write next chapter"]
Blocks can contain any number of values (up to the limit of memory) or no values at all. They can extend over multiple lines and can include any type of value, even other blocks:
[ ]
[24 37 108]
[REBOL [
Title: "Test Script"
Date: 30-Sep-1998
Author: "Ema User"
]]
Note that when words are used within a block (like name, work, home), they do not need to be previously defined. Their values may be set later in the script, or in some cases, not at all (as when they are being used symbolically).
There is no difference in REBOL between blocks that hold code and blocks that hold data. Code is simply a block that is evaluated and whose words when evaluated have meaning in REBOL. Evaluation is further discussed in Chapter 4 of this guide.
if date > 31/Mar/1999 [print "project delayed"]
while [time < 10:00] [
print time
time: time + 0:10
]
Blocks are free form. They are not sensitive to lines, spaces or tabs. You can place lines and spacing anywhere within the block, as long as it does not divide a single value.
Note: To make programs easier to read it is helpful to indent lines that fall within a block, as shown above. Note that the closing bracket is often placed on a separate line and is not indented like the content of the block.
Decomposing
A series is created so that it can be treated as a whole. However, to be useful, you will need to access the values within it. Because the values of a series have an order, the obvious way to extract a particular value is by specifying its position. The REBOL pick function does this:
>> colors: ["red" "green" "blue" "yellow" "orange"]
>> print pick colors 3
== blue
For convenience, REBOL also provides a few shorthand functions for picking values from common positions: first , second , third , fourth , fifth , and last .
>> print third colors
== blue
>> print last colors
== orange
Traversing
In REBOL, a common way to access a series is to traverse it (to move around inside it). To do anything more than pick out values, you will need to know this.
Traversing is like crossing a creek by stepping on stones. You start by hopping to the first stone (the head), then hop from one stone to the next until you reach the end (the tail). While you are in the process of moving across the creek your current position is marked by where you are standing. The same ideas apply to traversing a series.
In the previous section, the word colors was set to a block with:
colors: ["red" "green" "blue" "yellow" "orange"]
It is crucial for you to realize that the block exists on its own, and that colors simply refers to the head of the block.
When you traverse this block, you move to other positions within it. The entire block still exists, you are just at a different "stone" (to use the creek metaphor). For instance:
>> print colors
red green blue yellow orange
You are at the head of the block. The next function will "hop" to the next position in the block and return it. You could print from the new position or define a new word for it:
>> print next colors
green blue yellow orange
>> newer: next colors
>> print newer
green blue yellow orange
What is really important here is that you are still referring to the same block, just a different position within it. If you wanted, you could use the past function to move back to the position you just passed:
>> older: past newer
>> print older
red green blue yellow orange
Traversing in this fashion is the same for all series.
To obtain a value from a different position in the block, you can use the pick function or any of its shorthand functions (first , second , third , etc.):
>> print first newer
green
If you want to move forward or backward multiple positions, you could string together multiple next or back functions:
>> print next next next colors
yellow orange
However, for general cases it is better to use the skip function:
>> example: skip colors 3
>> print example
yellow orange
You can also go backwards in a series by providing a negative skip value.
If you want to skip forward to the tail (the position just after the last value) or back to the head, the tail and head functions can be used:
>> example: head example
>> print example
red green blue yellow orange
>> print past tail colors
orange
Notice in the last example you need to move backward one (past) before printing. That's because tail always places you just past the last value.
>> tail-colors: tail colors
>> print length? tail-colors
0
As you will learn later, tail is necessary in order to insert new values in the block.
Length
The length of a series is the number of values from the current position to its tail. The length? function will determine this.
>> print length? [10 20 30 40]
4
>> skipped: skip colors 3
>> print length? skipped
2
When a block appears as a value within another block, it only counts as one value:
>> values: [new [1 2 3] old [4 5]]
>> print length? values
4
>> print length? second values
3
Position
When you are traversing a series, there will be times when you want know your position from the head of the series. The index? function will provide you with that number.
>> skipped: skip colors 3
>> print index? skipped
4
You can also check to see if you are at the head or tail positions in a series with the head? and tail? functions:
>> if head? colors [print "at head"]
at head
>> if tail? colors [print "at tail"] else [print "not tail"]
not tail
>> while [not tail? colors] [
print [index? colors "-" first colors]
colors: next colors
]
1 - red
2 - green
3 - blue
4 - yellow
5 - orange
Note that colors is now at the tail position in the block. To move it back to the head:
>> colors: head colors
Find and Select
With the find function you can traverse to a particular value in a series:
>> found: find colors "blue"
>> print found
blue yellow orange
>> print first found
blue
Additional attributes can be specified to allow you to control the direction (backward or forward) as well as the pattern matching (like case sensitivity and wildcards).
A block is often used to hold related values in a simple "micro database". For instance:
email-book: [
"John" jd@great.effects.org
"Richard" rich@photo.edu
"Joe" walker@yoda.gov
"Ralph" RMQ@m.falcon.edu
]
To locate a person in this database:
>> name: find email-book "Joe"
>> print [first name "is at" second name]
Joe is at walker@yoda.gov
A handy variation of find is the select function, which will return the value that follows the one that was matched:
>> print select colors "blue"
yellow
>> print select email-book "Richard"
rich@photo.edu
The select function is commonly used for finding a particular block of code to evaluate (somewhat like the switch or case statements in other languages). An example:
cases: [
10 [print "ten"]
20 [name: "twenty"]
30 [quit]
]
>> do select cases 10
ten
Series Helpers
Traversing through an entire series is done often in REBOL. To help minimize the task, a few helper functions are provided.
The forall function will evaluate a block for all values from the current position to the tail of the series.
>> forall colors [print first colors]
red
green
blue
yellow
orange
Note that the first argument to forall must be a word that has been set to a series (in this case a block). As the series is traversed, the variable is updated to each position. When the function has finished, the variable is reset to its starting value.
The foreach function is similar, but is given a word and the series. For each value in the series, the word will be set to that value.
>> foreach color colors [print color]
red
green
blue
yellow
orange
The forskip function is similar to forall but skips a given number of values each time:
>> forskip colors 2 [print first colors]
red
blue
orange
Match
Without attributes, match compares to series and returns NONE if they are not the same, and the end position of the match if they are the same (which in the simple cases is usually the tail of the series). The result of a match is often passed to control functions such as if , while , or until .
str: "abcde"
if match str "abcde" [...]
If the /part function attribute is used, match will only compare the specified number of characters:
if match/part str "abc" 3 [...]
You can also specify the ending position of the match:
if match/part str "abc" find str "c" [...]
Note: Unlike the series modification functions, the position must be relative to the first series.
A match can also be made against wild card patterns. The /any attribute is used to indicate that the match can be made with any string that fits the pattern. Essentially, it allows you to use the same pattern matching features that you apply to file directories (as shown above). A '?' matches any character and a '*' matches zero or more chars.
match/any str "a*" ; any string starting with "a"
You can also use match for files, email, urls, etc.
match/any email-addr *@rebol.com
match/any file %*.r
match/any url http://www.rebol.com/*
Advanced REBOL users may need to use patterns containing the wild cards as characters. To do this, you can use /with to specify alternate wild chars with a string. The first char in the string is used as the char for single char matches (change from '?'), and the second is for any sequence (change from '*').
Creating Series
REBOL allows you to create new series with make and copy .
make creates a new series from a "prototype" and a parameter. The parameter can be an integer indicating the length (or additional length) of the series, or it can be the initial contents of the series.
str: make string! 100
file: make file! 10
file: make file! %myfile.txt
copy creates a new series by copying another. This is shorthand for make with a zero additional length parameter (make series 0). (The part option described below can also be used with this function.)
new-msg: copy message
new-msg-tail: insert (copy message) "Message: "
Modifying Series
REBOL offers a number of powerful functions to modify series. They include change, insert, remove, and clear.
change changes values within a series. If the second argument is a value, it will replace the value at the current position in the first series. If the second argument is a series, then its values will replace those of the first series. (The part , only , and dup options described below can also be used with this function.)
change block 200
change (skip block 5) 200
change tail block http://www.rebol.com
change block [200 "test"]
insert inserts values into a series. If the second argument is a value, it will be inserted into the series at the current position. If the second argument is a series, then its values will be inserted into the first series. (The part, only, and dup options described below can also be used with this function.)
insert block "start"
insert (find block 10:30) "time"
insert next block [200 "test"]
remove removes a value from a series. (The part option described below can also be used with this function.)
remove block ; removes first value from series
remove (find block fred@derf.com)
clear removes remaining values from a series.
clear skip block 20
clear find block http://www.microsoft.com
clear change name "sam" ; explained below
The change, insert, remove, and clear functions directly affect the series provided as the first argument. If you have other variables which refer to the same series, after the operation they may no longer reference the same value within the series.
The change , insert , and remove functions also return a result, which is the series as of the point just past the modification. This can be useful for making additional modifications:
>> start: "now is the time"
>> insert (remove/part find start "is" size? "is") "was"
>> print start
now was the time
; A simple replace function:
replace: make function! [series old new] [
use [here]
if (here: find series old) [
insert (remove here) new
] else [
print "not found"
]
]
In addition, options can be provided to modify functions. This is done by using function attributes (as paths, similar to those used with object references or file names):
change/part name "fred derf" 4
insert/only block [200 fred@derf.com]
change/dup space "*" 50
The valid options are:
part changes, inserts, or removes a given number of values (specified as either a count or as an ending series position).
change/part block items 5
insert/part string "new text" 3
remove/part string 5
remove/part string find string "end"
name: copy/part string 10
only treats a series value as a single value. Normally used to change or insert a block as a block, rather than as a series of values.
insert/only block ["Freddy" fred@derf.com
change/only (skip tail block -4) ["fred" 10]
dup repeats the change or insert for the number of times specified.
insert/dup block 'none 5
change/dup (find name "fred") "*" 4
change/part/dup name string 4 3
Dealing With None
If a series function attempts to access a value that is beyond the head or tail of a series, it will return a special value, none , to indicate that there was a problem. You can use the none? function to check for such a result:
>> color: pick colors 10
>> if none? color [print "no color"] else [print color]
no color
>> color: find colors "purple"
>> if none? color [print "cannot find purple"]
cannot find purple
Another function that is often used to detect none is the none? function, which returns true if a value exists and false if the value is none .
>> color: find colors "green"
>> if none? color [print [index? color first color]]
2 green
If you do not check for none and attempt to use it with a series function, an error will occur. In the example below, next does not like the none returned by the find .
>> print next find colors "purple"
** PROGRAM ERROR: no such action (next) for none **
The functions first, second, third, fourth, fifth, and last do not return none ; they will generate an error instead. This is done for the convenience of using these functions as shorthand. If you would prefer to receive a none in such situations, use the pick function.
>> sched: [12:30 "Lunch"]
>> print third sched
** PROGRAM ERROR: value is too short **
>> print pick sched 3
NONE