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
editI 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
editThis 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
editThe 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
editAs 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
editLet'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
editAnalogically 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
editDefinition (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
editTo 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
editThe 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
editLet'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
editObservation (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
editIn 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
editWe 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
editIn 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
editDefinition (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
editWhile 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
editWe 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
editThe 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
editWhile 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
editHaving implemented Rebol identity, let's try to define other interesting notions and functions.
Relatives
editExample (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
editIn 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
editLet'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
editYet 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
editIt 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
editApplication 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
editTwo 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
editWe 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
editdeepcopy: 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
editAttributes 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
editType-specific attributes:
character code
Chars are immutable.
Logic values
editType-specific attributes:
truth/falsity
Logic values are immutable.
Datatypes
editType-specific attributes:
datatype code
Datatypes are immutable.
Integers
editType-specific attributes:
number code (32-bit signed, two's complement, one's complement, or signed with magnitude)
Integers are immutable.
Decimals
editType-specific attributes:
number code (64-bit IEEE 754 binary floating point)
Decimals are immutable.
Money
editType-specific attributes:
currency code (three letters), number code (64-bit IEEE 754 binary floating point)
Money are immutable.
Pairs
editType-specific attributes:
x-coordinate (first coordinate, 32-bit signed integer), y-coordinate (second coordinate, 32-bit signed integer)
Pairs are immutable.
Dates
editType-specific attributes:
year, month, day, time, zone
Dates are immutable.
Time values
editType-specific attributes:
hour, minute, second
Time values are immutable.
Tuples
editType-specific attributes:
length, first byte, second byte, third byte, ... , tenth byte
Tuples are immutable.
Events
editType-specific attributes:
face, event type, offset, key, time, control, shift, double-click
Events are probably immutable.
Unbound words
editType-specific attributes (for more info on words see Bindology):
spelling
Unbound words are immutable.
Variables
editType-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
editType-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
editImages are like series with the distinction that they can be indexed using pairs.
Hashes
editHashes are essentially blocks using hash table for lookup.
Bitsets
editType-specific attributes are:
length, element references
Element references are volatile. (The insert and remove functions change element references.)
Structs
editType-specific attributes are:
spec, binary code
The binary code attribute is volatile.
Libraries
editType-specific attributes:
path, state
Both the path and state attributes are volatile.
Objects/contexts
editType-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
editRebol errors are relatives with objects. They can be transformed to objects using the disarm function.
Ports
editType-specific attributes:
index, ...
Ports are relatives with objects (see the relatives? function above). They are mutable.
Actions, natives, ops
editType-specific attributes:
spec, implementation
The spec attribute is volatile.
Routines
editType-specific attributes:
spec, ref
The spec attribute is volatile.
Functions
editType-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
editType-specific attributes:
set of datatypes
Typesets look immutable.
Symbols
editSymbols look more like a datatype leaked from the implementation than like a true Rebol datatype.
The end.