Rebol Programming/Advanced/Identity

Author: Ladislav Mecir

Rebol is an expression-oriented language. A successful evaluation of an expression yields a value. After successfully performing two expression evaluations we can legitimately ask whether we obtained two values or just one that is actually a result of both evaluations.

In the past, the question became a subject to dispute. The answers in the mailing list ranged from:

"Only the Rebol designer can answer such a question." (S opinion)

to:

"Two expression evaluations never yield one Rebol value. If the evaluations are discernible then their results can be discerned too." (N opinion)

The "S opinion" can be called "skeptic" since it expresses skepticism about our ability to find the correct answer to the question. In response to the "S opinion" we summarize the properties of Rebol identity and show that we are able to find out whether two successful expression evaluations yield one Rebol value. We do it by defining an identical? function "mechanically" doing just that.

Examining the "N opinion" one is tempted to call it "nihilistic" since it suggests that "two expression evaluations never yield one Rebol value". The acceptance of this idea has even more thorough consequences than the acceptance of the "S opinion", which at least allows us to ask Rebol designer when in doubt. As opposed to that, the "N opinion" might discourage us from reasoning about identity of Rebol values and from using it for practical purposes. Our definition of the identical? function looks sufficient to refute this opinion too, showing that Rebol identity exists and is practically usable.

During our quest for the Rebol identity we discuss other (once disputable) matters like "Is a given value mutable?" or "Which attributes a given value has?" and implement a couple of (hopefully interesting) functions.

Acknowledgments edit

I would like to say thanks to Joel Neely, Romano Paolo Tenca and many others who discussed the subject with me on the Rebol mailing list.

Notes edit

This version of the article was tested in Rebol/View 2.7.6.3.1. Other versions of the interpreter may differ in a few details.

A reader, wanting to check all the examples can run this code:

do http://www.rebol.org/download-a-script.r?script-name=identity.r

, which defines the functions from this article.

Relations edit

The goal of our quest, the identical? function has to accept any result or results of two expression evaluations and yield a logic! type value.

Let's call a function having these properties a relation.

Reflexivity edit

As is well known, we can refer to Rebol values using Rebol blocks or words. Example:

a-char: #"a"

The effect is that the a-char variable refers to the character as we expected:

a-char ; == #"a"

Let's now evaluate this expression:

a-block: head insert copy [] a-char

The effect is that now even the first entry of the above block refers to the character.

For the referring operation to behave consistently we expect that for any non-empty block the expression

identical? first block first block

shall yield true and the same consistency requirement leads us to expect that for any variable var the expression:

identical? get/any var get/any var

shall yield true too. This property we call reflexivity.

Symmetry edit

Let's have a block block having two entries such that the expression

identical? first block second block

yields true. Knowing that we can conclude that the two entries of the block refer to one value. If that is true then

identical? second block first block

shall yield true too. Using the same reasoning we expect that for every two variables var1 and var2 if

identical? get/any var1 get/any var2

yields true then the expression

identical? get/any var2 get/any var1

shall yield true too.

This property we call symmetry.

Transitivity edit

Analogically as above we can come to the conclusion that the identical? function shall have the following property:

If block is a block having three entries and both

identical? first block second block

and

identical? second block third block

yield true then

identical? first block third block

shall yield true too.

If var1, var2 and var3 are variables and both

identical? get/any var1 get/any var2

and

identical? get/any var2 get/any var3

yield true then

identical? get/any var1 get/any var3

shall yield true too.

This property we call transitivity.

Equivalence edit

Definition (Equivalence): We call any Rebol function having the above described properties (i.e. a relation that is reflexive, symmetric and transitive) an equivalence.

In accordance with our previous observations we restate that Rebol identity has to be an equivalence.

Let's use the definition to find out whether some functions are equivalences. For example, the never function implementing the "N opinion" from the summary

never: func [
    a [any-type!]
    b [any-type!]
] [false]

is a relation that is both symmetric and transitive. However, it isn't reflexive and therefore it isn't an equivalence.

On the other hand, the always function

always: func [
    a [any-type!]
    b [any-type!]
] [true]

is an equivalence, although a trivial one.

Observation: The same? function isn't an equivalence.

Proof:

  • The same? function is undefined for unset! and error! type values and therefore it isn't a relation.
  • The same? function is undefined for closed ports and therefore it isn't a relation.
block: reduce [make port! http://]
same? first block first block
;** Access Error: Port none not open
;** Near: same? first block first block
  • The same? function is undefined for money! values with different denominations and therefore it isn't a relation.
same? USD$1 EUR$1
** Script Error: USD$1.00 not same denomination as EUR$1.00
** Near: same? USD$1.00 EUR$1.00
  • The same? function does not return a logic! type value when obtaining struct! type values as arguments and therefore it isn't a relation.
block: reduce [make struct! [] none]
same? first block first block ; == none
  • The same? function isn't transitive when comparing decimal! values.
block: reduce [1.0 1.0 + 5e-16 1.0 + 1e-15]
same? first block second block ; == true
same? second block third block ; == true
same? first block third block ; == false
  • The same? function isn't transitive when comparing date! values.
block: [5/Sep/2006/0:00 5/Sep/2006 5/Sep/2006/1:00]
same? first block second block ; == true
same? second block third block ; == true
same? first block third block ; == false

Any of the above points suffices to demonstrate our observation q.e.d.

Observation: No equivalence comes built into the interpreter.

Actually, no relation comes built into the interpreter. This can be verified by examining every built-in function like we did in the case of the same? function.

Terminology: Let's have an equivalence eq and a block block having two entries. If the expression

eq first block second block

yields true, we say

"For the eq equivalence the value referenced by the first entry of the block block is equivalent to the value referenced by the second entry of the block block."

, or

"For the eq equivalence the value referenced by the first entry of the block block is indiscernible from the value referenced by the second entry of the block block."

If the above expression yields false, we say

"For the eq equivalence the value referenced by the first entry of the block block is not equivalent to the value referenced by the second entry of the block block."

, or

"For the eq equivalence the value referenced by the first entry of the block block is discernible from the value referenced by the second entry of the block block."

Fineness edit

To be able to compare equivalences we use

Definition (Fineness): We say that equivalence eq1 is finer or as fine as eq2 (resp. eq2 is coarser or as coarse as eq1) if for any block block having two entries for which

eq1 first block second block

yields true,

eq2 first block second block

yields true too.

Definition (Equal fineness): We say that equivalence eq1 is as fine as eq2 if for any block block having two entries holds that:

eq1 first block second block

yields true if and only if

eq2 first block second block

yields true.

Observation: The above defined always function is the coarsest equivalence.

Definition of Rebol identity edit

The notion of equivalence doesn't define identity completely since, e.g. the always function is an equivalence, but it isn't the identity we are looking for.

To define the identity we employ a fundamental logic principle known as

Observation (Indiscernibility of identicals): No value can be discerned from itself.

As a corollary to this principle we obtain

Observation (Leibniz's law): The identical? function has to be the finest equivalence in Rebol.

Proof: We demonstrated that identical? has to be an equivalence. Let's have an equivalence eq and a block block having two entries for which the expression

identical? first block second block

yields true. If the identical? function is the Rebol identity, we know that the first and second entries of the block refer to one Rebol value. According to the principle of indiscernibility of identicals the eq equivalence cannot discern first block from second block, which proves that identical? is finer or as fine as eq q.e.d.

We use Leibniz's law to define

Definition (Identity): We call an equivalence identity if it is the finest equivalence in Rebol.

Observation: Any two identities must be of equal fineness.

Proof: If we had two identities with different finenesses, one of them wouldn't be the finest q.e.d.

Observation: The above defined identity (if it exists) satisfies the principle of indiscernibility of identicals.

The proof is left as an exercise to the reader.

While we succeeded to uniquely define the identity, we are only half-way done since we didn't yet succeed to implement it.

To be able to implement the identity we need to collect more informations about Rebol values.

The type attribute edit

Let's note that every Rebol value has a type. A type-comparing equivalence can be defined as follows:

equal-type?: func [
    {do the values have equal type?}
    a [any-type!]
    b [any-type!]
] [equal? type? get/any 'a type? get/any 'b]

Observation: The equal-type? function discerns values with different datatypes.

Observation: The same? function does not always discern values that have different datatypes.

Proof: If we define

block: [3 3.0]

then

same? first block second block

yields true, while

equal-type? first block second block

yields false.

If we define

block: [a a:]

then

same? first block second block

yields true, while

equal-type? first block second block

yields false q.e.d.

The new-line attribute edit

Observation (the new-line attribute): Every Rebol value has a new-line attribute.

Proof: In the case of the type attribute we didn't need to prove that every Rebol value has a type since it is widely documented, there are many examples of values with various types and there is a type? native that returns a type of any given value.

To prove the existence of the new-line attribute we need to document it, provide examples of values with distinct new-line attribute values and define a new-line-attribute? function returning a new-line attribute of any given value.

We start with the documentation. The new-line attribute is a value that can be either true or false. It marks the positions of line breaks in a block. If a block entry refers to a value having the new-line attribute set to true, the mold function will insert a line break when molding the block.

The new-line-attribute? function examining the new-line attribute of any value can be defined as

new-line-attribute?: func [
    {returns the new-line attribute of a value}
    value [any-type!]
] [
    new-line? head insert/only copy [] get/any 'value
]

It isn't hard to observe that the new-line-attribute? function returns just true or false for any given value.

Another function called new-line-attribute can return values with the new-line attribute set as required

new-line-attribute: func [
    {returns a value with the new-line attribute set as specified}
    value [any-type!]
    attribute [logic!]
] [
    return first new-line head change/only [1] get/any 'value attribute
]

Now we can define

one: 1
new-line-attribute? one ; == false
one-nl: new-line-attribute 1 true
new-line-attribute? one-nl ; == true

, i.e. we successfully demonstrated that there are values with the new-line attribute set to false as well as values with the attribute set to true.

Now we demonstrate that molding a block having an entry referring to the one-nl value creates a string containing line-break

mold head insert copy [] one-nl ; == "[^/    1^/]"

This completes the proof of our new-line attribute observation q.e.d.

The knowledge of the new-line attribute and our above defined new-line-attribute? function can help us to define a function comparing the new-line attribute

equal-new-line?: func [
    {compares new-line attribute of the values}
    a [any-type!]
    b [any-type!]
] [
    equal? new-line-attribute? get/any 'a new-line-attribute? get/any 'b
]

It is easy to observe that the equal-new-line? is an equivalence discerning values with distinct new-line attributes. Illustration:

equal-new-line? one one ; == true
equal-new-line? one-nl one-nl ; == true
equal-new-line? one one-nl ; == false

On the other hand, it is equally easy to observe that the same? function does not discern values with distinct new-line attributes:

same? one one-nl ; == true

Functions that do not modify their arguments edit

In Rebol, some functions modify their arguments (it is usually stated in their documentation). It is not easy to write a simple definition of what the phrase "a function modifying its argument(s)" means, so, let's start with simple examples

f1: func [a] [2]

When observing the behaviour like

f1 1 ; == 2

, a reader unfamiliar with the meaning of the "function modifying its argument" phrase may be tempted to say that "f1 modified the argument value 1 to return the value 2". This is not the case since the modification we are trying to describe here has to be of a different nature.

To help us with the demonstration, let's use the apply function applying a given function to arguments submitted in a block (the function is defined in the %identity.r file).

In the case of the above f1 function we can use apply to obtain

apply/only :f1 arguments: [1] ; == 2

Note: we use the /only refinement of the apply function to make sure the function obtains the arguments contained in the block "as is".

If we examine the block of arguments after the call, we see that it looks unchanged, which suggests that the argument value hasn't been modified by f1

arguments ; == [1]

Let's see another example

f2: function [i [integer!]] [i + 1]
f2 1 ; == 2

Now again, one may be tempted to say that f2 changed the argument value, but when verified

apply/only :f2 arguments: [1] ; == 2
arguments ; == [1]

one observes that the block containing the argument value remains unchanged.

Another function, for which a reader may not immediately know whether the function modifies its argument is the new-line-attribute function defined above. Let's check

one-nl: apply/only :new-line-attribute arguments: [1 #[true]]
new-line-attribute? one-nl ; == true
new-line-attribute? first arguments ; == false

This shows that while the function yields a value with the new-line attribute set to true, the original argument value remains unchanged having the new-line attribute set to false.

Functions modifying their arguments edit

We use the set function as our first example of a function modifying its first (the word) argument.

a: [1]
arguments: [a [2]]
get first arguments ; == [1]
apply/only :set arguments
get first arguments ; == [2]

This example shows that the first argument (the variable a) originally referred to one block, while after being set it now refers to another block.

Our next example will examine the behaviour of the change function.

apply/only :change arguments: [[1] 2]
first arguments ; == [2]

In this case we see, that the change function changed its first argument too.

Note: functions may modify other values than their arguments too - e.g. it isn't hard to define a parameter-less function modifying something.

Expressions modifying their values edit

In the case of functions we illustrated the meaning of the phrase "a function modifying its argument". A question may arise, whether an expression modifies the value(s) in it or not.

We can convert the situation to the function case by defining an appropriate function and examining the behaviour of the function instead.

Example: let's find out whether an expression like

1 + 1

modifies the values in it or not. To find out, let's define a function as follows:

f: func [a b] [
    do reduce [a '+ b]
]

and supply the appropriate values as arguments

apply/only :f arguments: [1 1]
arguments ; == [1 1]

In this case our finding is that the expression actually doesn't modify the values in it.

Here is a case of an expression modifying the value in it

block/1: 2

, as we will find out examining

block: [1]
g: func [block value] [block/1: value]
arguments: reduce [block 2] ; == [[1] 2]
apply/only :g arguments
arguments ; == [[2] 2]

Contrast the above with a seemingly similar case

tuple: 1.1.1
h: func [tuple value] [tuple/1: value]
arguments: reduce [tuple 2] ; == [1.1.1 2]
apply/only :h arguments
arguments [1.1.1 2]

, which shows that in this case the first argument remains unmodified.

Pardon? The last example is the case of a situation, where a reader probably springs to attention. It is evident that even the last expression changed something! The only problem is that we didn't correctly guess what it was! So, let's try again, this time guessing that the expression modifies the variable

h2: func [variable value /local path expression] [
    path: to set-path! reduce [variable 1]
    expression: reduce [path value]
    do expression
]
tuple: 1.1.1
arguments: [tuple 2]
get first arguments ; == 1.1.1
apply/only :h2 arguments
get first arguments ; == 2.1.1

, and indeed, our second guess is proven correct.

Mutable and immutable values edit

Definition (Mutable values): Values that can be modified we call mutable values.

Definition (Immutable values): Values that cannot be modified we call immutable values.

Observation: Rebol variables are mutable (see the set and tuple examples).

Observation: Rebol blocks are mutable (see the change example).

Mutations used to discern values edit

While our excursion to the realms of modifying functions looked like a detour on our quest, now we show that mutations can be used to discern values. In the following example we define two bitsets

bs1: make bitset! #{00}
bs2: make bitset! #{00}
same? bs1 bs2 ; == true

, so, according to the same? function bs1 and bs2 are indiscernible. Let's define a special equivalence called equal-mutation? as follows

equal-mutation?: func [
    bs1 [any-type!]
    bs2 [any-type!]
    /local state1 state2
] [
    ; we concentrate on bitsets,
    ; so one of the criteria used is,
    ; whether the "bitsetness" of both values equals
    unless equal? bitset? get/any 'bs1 bitset? get/any 'bs2 [return false]
    
    ; to further concentrate on bitsets we consider non-bitsets equivalent
    unless bitset? get/any 'bs1 [return true]
    
    ; check whether both bitsets yield equal results
    ; when searching for #"^(00)"
    unless equal? state1: find bs1 #"^(00)" find bs2 #"^(00)" [return false]
    
    ; now the bitsets either both contain or don't contain #"^(00)"
    either state1 [
        ; both bitsets contain #"^(00)", so let's remove it from bs1
        remove/part bs1 "^(00)"
        
        ; we removed #"^(00)" from bs1,
        ; check, whether we find it in bs2
        state2: find bs2 #"^(00)"
        
        ; reverse the mutation
        insert bs1 "^(00)"
    ] [
        ; both bitsets don't contain #"^(00)", so let's insert it into bs1
        insert bs1 "^(00)"

        ; we inserted #"^(00)" into bs1,
        ; check, whether we find it in bs2
        state2: find bs2 #"^(00)"
        
        ; reverse the mutation
        remove/part bs1 "^(00)"
    ]
    
    ; bitsets are discernible, if STATE1 and STATE2 are equal
    state1 <> state2
]

This equivalence can discern the above bs1 and bs2 bitsets

equal-mutation? bs1 bs2 ; == false

Implementation of the identity edit

We have collected all the necessary informations. The same? function is close to our goal, so we will try to use it as much as possible and take care of the cases when it fails to do what we want.

Observation: The finest equivalence in Rebol is

identical?: func [
    {are the values identical?}
    a [any-type!]
    b [any-type!]
    /local statea stateb
] [
    case [
        ; compare types
        not-equal? type? get/any 'a type? get/any 'b [false]

        ; compare new-line attributes
        not-equal? new-line-attribute? get/any 'a
            new-line-attribute? get/any 'b [false]

        ; handle #[unset!]
        not value? 'a [true]

        ; errors can be disarmed and compared afterwards
        error? :a [same? disarm :a disarm :b]

        ; money with different denominations are discernible
        all [money? :a not-equal? first a first b] [false]

        (
            ; for money with equal denominations it suffices to compare values
            if money? :a [a: second a b: second b]
            decimal? :a
        ) [
            ; bitwise comparison is finer than same? and transitive for decimals
            statea: make struct! [a [decimal!]] none
            stateb: make struct! [b [decimal!]] none
            statea/a:  a
            stateb/b:  b
            equal? third statea third stateb
        ]

        ; this is finer than same? and transitive for dates
        date? :a [and~ a =? b a/time =? b/time]

        ; compare even the closed ports, do not ignore indices
        port? :a [
            error? try [statea: index? :a]
            error? try [stateb: index? :b]
            return and~
                statea = stateb ; ports with different indices are discernible
                equal? reduce [a] reduce [b]
        ]

        bitset? :a [
            ; bitsets differing in #"^(00)" are discernible
            either not-equal? statea: find a #"^(00)" find b #"^(00)" [false] [
                ; use the approach of the equal-mutation? equivalence
                either statea [
                    remove/part a "^(00)"
                    stateb: find b #"^(00)"
                    insert a "^(00)"
                ] [
                    insert a "^(00)"
                    stateb: find b #"^(00)"
                    remove/part a "^(00)"
                ]
                statea <> stateb
            ]
        ]

        ; for structs we compare third
        struct? :a [same? third a third b]

        true [:a =? :b]
    ]
]

Indices of series edit

The index? function gives us useful information about series. Unfortunately, it doesn't work as expected sometimes:

a: "1"
b: next a
index? b ; == 2
clear a
index? a ; == 1
index? b ; == 1
insert tail a #"1"
index? a ; == 1
index? b ; == 2

The example demonstrates that the index of the block b although printed as 1 once, was actually 2 all the time!

We can define a function able to yield a "more stable" value:

real-index?: func [
    {return a realistic index for any series}
    series [series!]
    /local orig-tail result
] [
    orig-tail: tail :series
    while [tail? :series] [insert tail :series #"1"]
    result: index? :series
    clear :orig-tail
    result
]

Test:

a: "11"
b: next a
clear a
index? b ; == 1
real-index? b ; == 2

Rebol identity is independent edit

While we used the same? function above to obtain an "efficient" implementation of identical?, identical? actually doesn't depend on the same? function.

Proof: We can write another (even though a less efficient) implementation of identical? not using same? or =? at all:

id2?: func [
    {are the values identical?}
    a [any-type!]
    b [any-type!]
    /local statea stateb
] [
    case [
        ; compare types first
        not-equal? type? get/any 'a type? get/any 'b [false]

        ; compare new-line attributes
        not-equal? new-line-attribute? get/any 'a
            new-line-attribute? get/any 'b [false]

        ; handle #[unset!]
        not value? 'a [true]

        ; errors can be disarmed and compared afterwards
        error? :a [equal? disarm :a disarm :b]

        ; money with different denominations are discernible
        all [money? :a not-equal? first a first b] [false]

        (
            ; for money with equal denominations it suffices to compare values
            if money? :a [a: second a b:  second b]
            decimal? :a
        ) [
            ; bitwise comparison is finer than same? and transitive for decimals
            statea: make struct! [a [decimal!]] none
            stateb: make struct! [b [decimal!]] none
            statea/a:  a
            stateb/b:  b
            equal? third statea third stateb
        ]

        ; this is finer than same? and transitive for dates
        date? :a [and~ a = b a/time = b/time]

        ; compare even the closed ports, do not ignore indices
        port? :a [
            error? try [statea: index? :a]
            error? try [stateb: index? :b]
            return and~
                statea = stateb ; ports with different indices are discernible
                equal? reduce [a] reduce [b]
        ]

        bitset? :a [
            ; bitsets differing in #"^(00)" are discernible
            either not-equal? statea: find a #"^(00)" find b #"^(00)" [false] [
                ; use the approach of the equal-mutation? equivalence
                either statea [
                    remove/part a "^(00)"
                    stateb: find b #"^(00)"
                    insert a "^(00)"
                ] [
                    insert a "^(00)"
                    stateb: find b #"^(00)"
                    remove/part a "^(00)"
                ]
                statea <> stateb
            ]
        ]

        (
            ; for structs we compare third
            if struct? :a [a: third a b: third b]

            series? :a
        ) [
            either equal? real-index? :a real-index? :b [
                ; A and B have equal index, it is sufficient to compare tails
                a: tail :a
                b: tail :b

                ; use INSERT to mutate A
                insert a #"1"
                stateb: 1 = length? b

                ; undo the mutation
                clear a
                stateb
            ] [false]
        ]

        any-word? :a [
            ; compare spelling
            either not-strict-equal? mold :a mold :b [false] [
                ; compare binding
                equal? bind? :a bind? :b
            ]
        ]

        true [:a = :b]
    ]
]

Discussion: To some readers this finding may look only like a coincidence. To explain why it is not a coincidence, consider that the user might restrict the language unsetting the same? function. We demonstrated that the restricted language would still have its identity, and that the identity would be "fully featured" in the sense that it would reflect all attributes of Rebol values accessible to the user. If the addition of the same? function added any additional attribute, such an attribute would not be really useful, being accessible solely to the same? function.

Applications edit

Having implemented Rebol identity, let's try to define other interesting notions and functions.

Relatives edit

Example (Errors and objects):

error? f: make error! "OK"
error? g: f
h: disarm f
error? i: make error! "OK"
; now all the values look the same
probe disarm f
probe disarm g
probe h
probe disarm i
h/arg1: "KO"
probe disarm f
probe disarm g
probe h
probe disarm i

In this example the change affected the object referenced by the word h and the error referenced by f and g, while it didn't affect the error value referenced by i. We can say that the values referenced by g and h, while not being the same, are relatives in a sense.

A similar property is observable for words too:

set 'a 1
alias 'a "aa"
set 'aa 2
get 'a ; == 2
same? 'a 'aa ; == false

Let's define an equivalence coarser than identical? corresponding to the property we have seen in the previous examples. The relatives? equivalence yields true if every change of its first argument is a change of its second argument and vice versa, every change of its second argument is a change of its first argument.

relatives?: func [
    {
        Two values are relatives, if every change of one
        affects the other too
    }
    a [any-type!]
    b [any-type!]
    /local var var2
] [
    ; errors are relatives with objects
    if error? get/any 'a [a: disarm :a]
    if error? get/any 'b [b: disarm :b]
    ; ports are relatives with contexts
    if port? get/any 'a [a: bind? in :a 'self]
    if port? get/any 'b [b: bind? in :b 'self]
    ; objects
    if not-equal? object? get/any 'a object? get/any 'b [return false]
    if object? get/any 'a [
        ; objects are relatives with contexts
        a: bind? in :a first first :a
        b: bind? in :b first first :b
        return same? :a :b
    ]
    ; structs
    if not-equal? struct? get/any 'a struct? get/any 'b [return false]
    if struct? get/any 'a [return same? third :a third :b]
    ; series
    if not-equal? series? get/any 'a series? get/any 'b [return false]
    if series? get/any 'a [
        if not-equal? list? :a list? :b [return false]
        ; series with different indices can be relatives
        a: tail :a
        b: tail :b
        unless list? :a [
            ; any-blocks are relatives with blocks
            ; any-strings are relatives with strings
            parse :a [a:]
            parse :b [b:]
        ]
        return same? :a :b
    ]
    ; variables
    if not-equal? all [
        any-word? get/any 'a bind? :a ; is it a variable?
    ] all [
        any-word? get/any 'b bind? :b ; is it a variable?
    ] [return false]
    if all [any-word? get/any 'a bind? :a] [
        return found? all [
            equal? :a :b
            same? bind? :a bind? :b
        ]
    ]
    ; functions
    if not-equal? any-function? get/any 'a any-function? get/any 'b [
        return false
    ]
    if any-function? get/any 'a [return same? :a :b]
    ; bitsets
    if not-equal? bitset? get/any 'a bitset? get/any 'b [return false]
    if bitset? get/any 'a [
        unless equal? var: find a #"^(00)" find b #"^(00)" [return false]
        either var [
            remove/part a "^(00)"
            var2: find b #"^(00)"
            insert a "^(00)"
        ] [
            insert a "^(00)"
            var2: find b #"^(00)"
            remove/part a "^(00)"
        ]
        return var <> var2 
    ]
    ; all other values
    true
]

Usage:

a: [1]
insert a reduce [a]
b: [1]
insert b reduce [b]
relatives? a a/1 ; == true
relatives? b b/1 ; == true
relatives? a b ; == false
a: [1]
b: tail a
remove a
relatives? b b ; == true

a: "11"
b: next a
relatives? a b ; == true
index? a ; == 1
index? b ; == 2

Identity of references edit

In the example showing that the set function modifies its first argument we caused the variable a' to refer to another block. A different formulation may look as follows: "We changed the variable a's reference."

Variables and references edit

Let's see another example of the behaviour:

alias 'a "ax"
a: [1]
b: a
a ; == [1]
b ; == [1]
a: [2]
a ; == [2]
ax ; == [2]
b ; == [1]

Similarly as before we can say that we didn't change the block, but in this case it looks that we changed the a's as well as the ax's reference. Indeed, a and ax are aliases and use a common reference, which means that a's reference and ax's reference are just different names for one reference.

In other words, we can speak about the identity of references too. The same-variable? function from [Contexts] checks identity of references of its argument words.

Series and references edit

Yet another example of the behaviour:

c: []
insert/only tail c [1]
insert/only tail c first c
d: next c
first c ; == [1]
second c ; == [1]
first d ; == [1]
poke c 2 [2]
first c ; == [1]
second c ; == [2]
first d ; == [2]

Our question: "Did we change the block referred to by second c?" The answer is no, similarly as above, because the block referred to by second c was referred to by first c too and we see no change of first c. The conclusion: "We changed the second c reference only." Moreover, we see that both second c and first d are only different names of one reference.

Here is a function checking the identity of two series references. The function uses the PICK function's indexing convention, although a less complicated indexing would lead to simpler implementation:

same-series-references?: func [
    {
        Find out, whether the INDEX1 reference in the SERIES1
        is the same as
        the INDEX2 reference in the SERIES2
    }
    series1 [series!]
    index1 [integer!]
    series2 [series!]
    index2 [integer!]
] [
    if zero? index1 [return zero? index2]
    if zero? index2 [return false]
    index1: either negative? index1 [index1] [index1 - 1]
    index2: either negative? index2 [index2] [index2 - 1]
    found? all [
        relatives? :series1 :series2
        equal? (real-index? :series1) + index1
            (real-index? :series2) + index2
    ]
]

Let's check our previous finding:

same-series-references? c 2 d 1 ; == true

Unfortunately, this isn't the whole truth. Although our implementation and findings are correct, some access methods don't work for series in some state. That is why we obtain:

clear c
same-series-references? c 2 d 1 ; == true
pick c 2 ; == none
pick d 1
** Script Error: Out of range or past end
** Near: pick d 1

, which looks as if the references weren't the same, although the difference is caused by pick, which didn't use the reference after examining d.

An attentive reader may notice that we left out the identity of bitset references, but that case is rather simple, so it can be left as an exercise for the reader.

Finding a reference to a value edit

It might be useful to have a function able to find a reference to a given value:

find-reference: func [
    {find a reference to a given value in a series}
    series [series!]
    value [any-type!]
] [
    while [not tail? :series] [
        if identical? first :series get/any 'value [
            return :series
        ]
        series: next :series
    ]
    none
]

Another function of this kind may be a function searching for a relative of a given value:

find-relative: func [
    {find a reference to a relative of a value in a given series}
    series [series!]
    value [any-type!]
] [
    while [not tail? :series] [
        if relatives? first :series get/any 'value [
            return :series
        ]
        series: next :series
    ]
    none
]

Finding values with a given property edit

Application of the find-reference function, a function able to recursively find out whether a block or its subblocks contain a value with a given property. The function works even for cyclic blocks:

rfind: function [
    {
        find out whether a block
        or its subblocks
        contain a value with a given property
    }
    block [block!]
    property [any-function!]
] [rf explored] [
    explored: make block! 0
    rf: function [
        block
    ] [result] [
        if not find-reference explored block [
            insert/only tail explored block
            while [not tail? block] [
                either (property first block) [
                    return block
                ] [
                    if all [
                        block? first block
                        result: rf first block
                    ] [return result]
                ]
                block: next block
            ]
        ]
        none
    ]
    rf block
]

Comparing state edit

Two values may be in equal state even if they aren't identical. Let's define another equivalence able to find out whether two Rebol values are in equal state. Because the defined function will be an equivalence, it will not have the main disadvantages of the equal?, strict-equal?, etc. functions.

There is a problem with complicated values. Two functions shouldn't be found equal if they can yield different results. It is impossible to define a general comparing algorithm that would be able to find out whether two complicated functions are equal if they aren't the same. That is why I use a trivial approach (find out whether the values are identical) for functions. This approach is used for ports too.

The below defined equal-state? function will test every possible attribute of values that is available without their mutation. For some applications coarser equivalences may be more suitable.

An auxiliary function:

find-pair: func [
    {find a pair of occurrences in a given series}
    series [series!]
    a [any-type!]
    b [any-type!]
] [
    while [not tail? :series] [
        if all [
            identical? first first :series get/any 'a
            identical? second first :series get/any 'b
        ] [return :series]
        series: next :series
    ]
    none
]

equal-state?: function [
    {are the values in equal state?}
    a [any-type!]
    b [any-type!]
] [compo compb compw rc] [
    compo: make block! 0
    compb: make block! 0
    compw: make block! 0
    rc: function [
        a [any-type!]
        b [any-type!]
    ] [i1 i2] [
        unless equal-type? get/any 'a get/any 'b [return false]
        unless equal-new-line? get/any 'a get/any 'b [return false]
        if identical? get/any 'a get/any 'b [return true]
        if error? :a [
            a: disarm :a
            b: disarm :b
        ]
        if object? :a [
            if find-pair compo :a :b [return true]
            insert/only tail compo reduce [:a :b]
            return rc bind first a in a 'self bind first b in b 'self
        ]
        if any-word? :a [
            if strict-not-equal? mold :a mold :b [return false]
            if find-pair compw :a :b [return true]
            insert/only tail compw reduce [:a :b]
            return rc get/any :a get/any :b
        ]
        if struct? :a [
            return found? all [
                equal? first :a first :b
                equal? second :a second :b
                equal? third :a third :b
            ]
        ]
        if series? :a [
            error? try [i1: index? :a]
            error? try [i2: index? :b]
            if not-equal? i1 i2 [return false]
            a: head :a
            b: head :b
            if not-equal? length? :a length? :b [return false]
            if any-string? :a [return strict-equal? :a :b]
            if find-pair compb :a :b [return true]
            insert/only tail compb reduce [:a :b]
            repeat i length? :a [
                unless rc pick :a i pick :b i [return false]
            ]
            return true
        ]
        false
    ]
    rc get/any 'a get/any 'b
]

Observation (Another informal description of identical?): Two Rebol expression evaluations yield one value, if the state of the result of the first expression evaluation is equal to the state of the result of the second expression evaluation and every mutation of the result of the first expression evaluation is a mutation of the result of the second expression evaluation.

Corollary: Two expression evaluations in Rebol yield one value if the result is immutable and the state of the result of the first expression evaluation is equal to the state of the result of the second expression evaluation.

Finding out whether a given block is cyclic edit

We can use a lot of different definitions of block cyclicity. The first one is probably the most strict:

strict-cyclic?: function [
    block [any-block!]
] [rec in] [
    in: make block! 1
    rec: func [checked] [
        if not positive? real-length? :checked [
            return false
        ]
        if find-reference in :checked [
            return true
        ]
        insert/only tail in :checked
        foreach value :checked [
            if all [
                any-block? get/any 'value
                rec :value
            ] [return true]
        ]
        remove back tail in
        false
    ]
    rec :block
]

Here is another one that yields results more closely related to results of native functions:

native-cyclic?: function [
    block [any-block!]
] [rec in] [
    in: make block! 1
    rec: func [checked] [
        if not positive? real-length? :checked [
            return false
        ]
        if find-relative in :checked [
            return true
        ]
        insert/only tail in :checked
        foreach value :checked [
            if all [
                any-block? get/any 'value
                rec :value
            ] [return true]
        ]
        remove back tail in
        false
    ]
    rec :block
]

Making a deep copy of a (possibly cyclic) block edit

deepcopy: function [
    block [any-block!]
] [rc copied copies] [
    copied: make block! 0
    copies: make block! 0
    rc: function [
        block
    ] [result found] [
        either found: find-reference :copied :block [
            return pick copies index? found
        ] [
            result: make :block :block
            insert/only tail copied :block
            insert/only tail copies :result
            while [not tail? :result] [
                if any-block? first :result [
                    change/only :result rc first :result
                ]
                result: next :result
            ]
            head :result
        ]
    ]
    rc :block
]

Attributes of Rebol values edit

Attributes of Rebol values are the values that are associated with them. The state of a Rebol value is a "composition" of its attributes. As discussed above, every Rebol value has these general attributes:

  • type attribute,
  • and new-line attribute

For the values of none! and unset! types, the above are their only attributes, i.e., the values of the none! and unset! types do not have any type-specific attribute.

When a Rebol value is changed, at least one of its attributes changes. Let's call an attribute of a Rebol value that can be changed a volatile attribute, and an attribute that cannot be changed a regular attribute.

Observation (Attributes and mutability): A value is mutable if it has at least one volatile attribute.

Observation: Both the type and the new-line are regular attributes.

Other types have additional type-specific attributes listed below.

Chars edit

Type-specific attributes:

character code

Chars are immutable.

Logic values edit

Type-specific attributes:

truth/falsity

Logic values are immutable.

Datatypes edit

Type-specific attributes:

datatype code

Datatypes are immutable.

Integers edit

Type-specific attributes:

number code (32-bit signed, two's complement, one's complement, or signed with magnitude)

Integers are immutable.

Decimals edit

Type-specific attributes:

number code (64-bit IEEE 754 binary floating point)

Decimals are immutable.

Money edit

Type-specific attributes:

currency code (three letters), number code (64-bit IEEE 754 binary floating point)

Money are immutable.

Pairs edit

Type-specific attributes:

x-coordinate (first coordinate, 32-bit signed integer), y-coordinate (second coordinate, 32-bit signed integer)

Pairs are immutable.

Dates edit

Type-specific attributes:

year, month, day, time, zone

Dates are immutable.

Time values edit

Type-specific attributes:

hour, minute, second

Time values are immutable.

Tuples edit

Type-specific attributes:

length, first byte, second byte, third byte, ... , tenth byte

Tuples are immutable.

Events edit

Type-specific attributes:

face, event type, offset, key, time, control, shift, double-click

Events are probably immutable.

Unbound words edit

Type-specific attributes (for more info on words see Bindology):

spelling

Unbound words are immutable.

Variables edit

Type-specific attributes (for more info on words see Bindology):

spelling, binding, value reference

The value reference attribute is volatile; Rebol variables can refer to different values during their lifetime.

Series edit

Type-specific attributes:

index, length, element references

The length attribute is volatile (can be changed by the insert function), element references are volatile too (can be changed by the change function).

The index attribute of any-strings and any-blocks is regular as opposed to lists, where it is volatile.

The subtypes of series are:

  • any-string!
  • any-block!
  • list!

The any-string! datatype consists from: binary!, email!, file!, issue!, string!, tag! and url!

The any-block! datatype consist from: block!, get-path!, lit-path!, paren!, path!, and set-path!

Images edit

Images are like series with the distinction that they can be indexed using pairs.

Hashes edit

Hashes are essentially blocks using hash table for lookup.

Bitsets edit

Type-specific attributes are:

length, element references

Element references are volatile. (The insert and remove functions change element references.)

Structs edit

Type-specific attributes are:

spec, binary code

The binary code attribute is volatile.

Libraries edit

Type-specific attributes:

path, state

Both the path and state attributes are volatile.

Objects/contexts edit

Type-specific attributes:

set of variables

The set of variables of an object cannot be enlarged, except for the global context. The global context can be enlarged using make, to and load functions.

Individual variables are volatile (see above).

Errors edit

Rebol errors are relatives with objects. They can be transformed to objects using the disarm function.

Ports edit

Type-specific attributes:

index, ...

Ports are relatives with objects (see the relatives? function above). They are mutable.

Actions, natives, ops edit

Type-specific attributes:

spec, implementation

The spec attribute is volatile.

Routines edit

Type-specific attributes:

spec, ref

The spec attribute is volatile.

Functions edit

Type-specific attributes:

spec, body, function context (similar to object)

The spec and body attributes are volatile, individual variables in the function context are volatile too.

Typesets edit

Type-specific attributes:

set of datatypes

Typesets look immutable.

Symbols edit

Symbols look more like a datatype leaked from the implementation than like a true Rebol datatype.

The end.