Originally published in Rebol Forces.

Reb-IT!
Author: Ladislav Mecir
Date: Jan, 2001

Contents

Overview

The purpose of this article is not to provide a text covering all the features of Series, but rather to provide a reader with a method useful for finding answers to all Series questions and help to understand the inner workings of Series. The method used herein does not use models of Series written in strange languages. It could be probably described as an axiomatic and Rebol-friendly method.

Introducing Series, Places and Elements

A Series is a value, useful for value storage.

series1: [1 2 3] ; == [1 2 3]

A Series consists of a number of Places, that are able to contain values. The Places of a Series are numbered as usual, i.e. first, second, ...

The number of Places of a Series we call its Length. The Length can be determined using the LENGTH? action.

length? [1 2 3] ; == 3

A Series can be Empty, i.e. consisting of no Places at all.

series2: []     ; == []
empty? series2 ; == true

Knowing this, we could create our own version of the EMPTY? native for Series, if we wished. Let's use a modified name to preserve the original native.

empty?-def: func [
{Returns true, if a series is empty.}
series [series!]
][
0 = length? :series
]
empty?-def [] ; == true
empty?-def [1] ; == false

For any Place of a Series we can pick a value stored at that place.

pick [1 2 3] 2  ; == 2

Instead of saying: "the value stored at the second Place of a Series" we can use shorter and more usual: "the second Element of a Series".

Let's discover a less natural property of Series. One value can be stored at multiple Places in a Series at the same time.

series3: [1 1] ; == [1 1]
series4: [o1 o1] ; == [o1 o1]
o1: make object! [attribute: "OK"]
 
series5: reduce series4
; == [
; make object! [
; attribute: "OK"
; ]
o1/attribute: "Surprise!" ; == "Surprise!"
 
series5
; == [
; make object! [
; attribute: "Surprise!"
; ]

Analogically, one value can be stored in multiple Series at the same time.

o2: make object! [attribute: "OK"]
series6: reduce [o2 1]
; == [
; make object! [
; attribute: "OK"
; ] 1]
 
series7: reduce [o2 2]
; == [
; make object! [
; attribute: "OK"
; ] 2]
o2/attribute: "Surprised Again?" ; == "Surprised Again!"
 
series6
; == [
; make object! [
; attribute: "Surprised Again?"
; ] 1]
 
series7
; == [
; make object! [
; attribute: "Surprised Again?"
; ] 2]

From Head to Tail and back again

For a given Series we can specify a Series consisting of the Places of the given Series except for the first skipped Place. That means, that the first Place of the newly specified Series will be the second Place of the original Series etc. No Place is destroyed by NEXT.

series8: [1 2 3 4]     ; == [1 2 3 4]
series9: next series8 ; == [2 3 4]

Instead of skipping one Place at a time, we can skip more Places at once using the SKIP action. A Rebol definition of the SKIP action for non-negative OFFSET:

skip-def: func [
{Skips some places of a series}
series [series!]
offset [integer!] {Can be positive, or zero.}
][
for i 1 offset 1 [
series: next :series
]
:series
]
series8: [1 2 3 4] ; == [1 2 3 4]
series10: skip-def series8 2 ; == [3 4]

If we skip zero Places, we obtain the original Series.

series8: [1 2 3 4]            ; == [1 2 3 4]
same? series8 skip series8 0 ; == true

If we skip all Places of a Series, we obtain an Empty Series called its Tail. The definition:

tail-def: func [
{Returns the tail of a series.}
series [series!]
][
skip :series length? :series
]
tail-def [1 2 3] ; == []

Here is a definition of a function able to decide, if a Series is a Tail.

tail?-def: func [
{Finds out, if a given Series is a Tail}
series [series!]
][
empty? :series
]
 
tail?-def [1 2 3] ; == false
tail?-def [] ; == true

In an Empty Series there aren't any Places, that can be skipped. Skipping any further has the same effect as skipping zero Places then. That fact can be used to provide a different version of the TAIL?-DEF:

tail?-def-2: func [
{Finds out, if a given Series is a Tail}
series [series!]
][
same? :series next :series
]
 
tail?-def-2 [1 2 3] ; == false
tail?-def-2 [] ; == true

We can use the BACK action to skip one Place backwards, which means the opposite of NEXT.

series11: tail [1 2 3 4]   ; == []
series12: back series11 ; == [4]

Let's enhance the SKIP-DEF to use negative OFFSET values.

skip-def: func [
{Skips some places of a series}
series [series!]
offset [integer!] {Can be positive, zero, or negative.}
][
either positive? offset [
for i 1 offset 1 [
series: next :series
]
][
for i -1 offset -1 [
series: back :series
]
]
:series
]
 
series11: tail [1 2 3 4] ; == []
series13: skip-def series11 -2 ; == [3 4]

No Places are created when skipping backwards, only existing Places can be skipped. This implies, that for every Series there is a maximum number of Places, that can be skipped backwards. The Series we obtain from a given Series by skipping the maximum possible number of Places backwards is called the Head of the given Series.<br>

series14: next [1 2 3]    ; == [2 3]
head series14 ; == [1 2 3]

We can use the above definition to provide a Rebol function determining, if a Series is a Head.

head?-def: func [
{Returns true for a head series.}
series [series!]
][
same? :series back :series
]
 
head?-def [] ; == true

With a help of HEAD? we can even provide the definition of HEAD.

head-def: func [
{Returns the head of a series.}
series [series!]
][
while [not head? :series] [series: back :series]
:series
]

Now let's define the Skipped number of a Series. Skipped is the number of Places we must skip backwards to get the Head. (an equivalent definition is, that Skipped is the number of Places of the Head we must skip, to get the given Series):

skipped?: func [
{
Returns the number of Places
we must skip backwards to get the head.
}
series [series!]
][
(length? head :series) - (length? :series)
]
 
skipped? tail [1 2 3] ; == 3

Rebol/Core doesn't contain the SKIPPED? function. Instead it contains the INDEX? action, defined as:

index?-def: func [
{Returns the index of a series.}
series [series!]
][
1 + skipped? :series
]
 
index?-def skip [4 5 6] 2 ; == 3

An equivalent description of INDEX? is, that for a Non Empty Series it returns the number of the first Place of the given Series in its Head, while for an Empty Series it returns the Length of the Head increased by one.

Changing Series

If we want to change a Series, we can use the CHANGE action. CHANGE doesn't alter the Elements of a Series, rather it replaces them.

a: make object! [attribute: "OK"]
 
series15: reduce [a a]
; == [
; make object! [
; attribute: "OK"
; ]
; make object! [
; attribute: "OK"
; ]]
 
change series15 2
; == [
; make object! [
; attribute: "OK"
; ]]
 
series15
; == [2
; make object! [
; attribute: "OK"
; ]]

CHANGE doesn't return the original Series. It skips the affected Places to facilitate subsequent changes.<br>

Comparing Places and Series

For any Series we can use the COPY action to create a new Series containing the same elements. A copy of a Series is not the same Series as the original, though.<br>

series16: reduce [make object! [attribute: "OK"]]
; == [
; make object! [
; attribute: "OK"
; ]]
 
series17: copy series16
>; == [
; make object! [
; attribute: "OK"
; ]]
 
same? series16 series17 ; == false
change series16 1 ; == []
series16 ; == [1]
series17
; == [
; make object! [
; attribute: "OK"
; ]]

Although Places are not directly available in Rebol, we can specify a Place by specifying a Series and a number of the Place in the Series. We can find out, if two Places specified are the same. This version works only for block Places, but a more general, and more complicated version is possible.

same-places?: function [
{Find out, if two Places are the same}
[catch]
series1 [block!]
number1 [integer!]
series2 [block!]
number2 [integer!]
][result original] [
; check, if the arguments really specify places
if any [
number1 <= 0
number1 > length? :series1
] [throw make error! {Wrong first place spec!}]
if any [
number2 <= 0
number2 > length? :series2
] [throw make error! {Wrong second place spec!}]
series1: skip :series1 number1 - 1
original: copy/part :series1 1
result: found? all [
(
change :series1 1
equal? 1 pick :series2 number2
)
(
change :series1 2
equal? 2 pick :series2 number2
)
]
change :series1 :original
result
]
 
same-places? series18: [1 2 3] 2 next series18 1 ; == true

Now, when we are able to compare Places, we can say, that two Non Empty Series are the same if they consist of the same Places. The trouble with this method is, that it is not usable for Empty Series, because in an Empty Series there is no Place we could use for the comparison purposes.

We call two Series mutually Independent, if they neither share any Places, nor they can be derived from one "mother" Series using the SKIP function. In Rebol this definition can be written as follows:

independent?: func [
{Find out, if two series are mutually independent.}
series1 [series!]
series2 [series!]
] [
not same? head :series1 head :series2
]
series19: [1 2 3] ; == [1 2 3]
series20: next series19 ; == [2 3]
series21: copy series19 ; == [1 2 3]
independent? series19 series20 ; == false
independent? series19 series21 ; == true

Observation: copies of a Series are mutually Independent.

The Cumulative properties of Series operations

A cumulative property of CHANGE/ONLY: CHANGE/ONLY replaces the Element stored at the first Place of its argument Series. From this description is immediately obvious, that CHANGE/ONLY affects all Series sharing the Place in question.

We can create a new Place using the INSERT action.

series22: [1 2 3]         ; == [1 2 3]
length? series22 ; == 3
insert/only series22 0 ; == [1 2 3]
series22 ; == [0 1 2 3]
length? series22 ; == 4

We can use INSERT to define the meaning of the SAME? action for Empty Series. We say, that two Empty Series are the same, if INSERT used for the first of the Series causes the second Series to become Non Empty too.

The description of the INSERT/ONLY work: INSERT/ONLY creates a new Place and inserts it into the Series, enlarging the number of the Places of the Series by one. The newly inserted Place becomes the first Place of the Series, while the numbers of the other Places of the Series are therefore higher by one. INSERT/ONLY stores its VALUE argument at the new Place. Note, that INSERT/ONLY doesn't change Index of any Series! It skips the inserted Places to facilitate subsequent inserts.

The cumulative property of INSERT/ONLY: for all Series Dependent on the Series affected by INSERT/ONLY holds: the Dependent Series is enlarged by one Place too, moreover, if the Index of the Dependent Series is lower than the Index of the affected Series, its first Place remains the same, if the Index of the Dependent Series is equal to the Index of the affected Series, the first Place of the Series after Insert will be the newly inserted Place and if the Index of the Dependent Series is higher than the Index of the affected Series, then the first Place of the Dependent Series will be the Place preceding the original first Place.

For lowering the number of the Places in a Series we can use the REMOVE and CLEAR actions. Probably every reader is now able to use the above described methods for exploring the cumulative properties of these. I shall leave that as an exercise for the reader.

Ladislav's Website