Rebol 程式設計/語言特性/序列
序列 由其設計者引入 Rebol,以表示值序列。 Rebol 中最典型的序列是塊。
自然地,如果你能夠訪問、操作和引用序列中的單個條目,那將非常方便。
事實上,Rebol 提供了一個相當廣泛的“資料訪問技術”來實現這一點。
讓我們看一個第一個例子,這將有助於我們更直觀地理解序列 的樣子。
以下是包含兩個條目的 Rebol 塊
series1: [1 2] ; == [1 2]
你可以將其解讀為
| “ | 詞語 series1 指的是一個包含 2 個條目的塊,這兩個條目都恰好引用整數。 | ” |
由於任何塊都包含零個或多個條目(你知道塊可以是空的,對吧?),並且事實上,這個特定的塊恰好包含 2 個條目,所以你可以說該塊可以被視為一個序列。
當然,當你看到這一點時,你可能會想,如何“詢問”第二個條目的值是什麼?或者你可能想知道某個序列中的第 n 個條目是否存在?或者你可能希望改變序列中的特定條目。
你可以在 Rebol 中完成所有這些以及更多操作。
所以讓我們看看如何在 Rebol 中訪問序列中的資料。
在 Rebol 中,我們可以使用幾種方法。 使用 first、second 等函式的方法
first series1 ; == 1
使用路徑表示法 的方法
series1/1 ; == 1
使用 pick 函式的方法
pick series1 1 ; == 1
假設我們有一個變數(例如 i),
i: 2
它將用於訪問序列中的特定條目。 如何做到這一點?
雖然使用 first 函式的方法看起來對這種情況不太有用,但它也可以使用。 詳細內容將在後續章節中介紹。
使用路徑表示法的方法是
series1/:i ; == 2
使用 pick 函式的方法
pick series1 i ; == 2
實際上,series/i 確實 起作用,但它不將詞語 i 解釋為變數。 例如
i: 20 series23: [1 i 3] series23/i ; == 3
說明:當直譯器遇到series23/i表示式時,它試圖在 series23 中找到詞語 i,即它將該詞語視為一個值,而不是一個變數。 返回的值是緊挨著已找到值的下一個值。
但從核心 2.6.0 開始,series/(i) 已經實現了
i: 1 series23: [1 i 3] series23/(i) ; == 1
透過將詞語 i 放在括號中,括號表示式將被求值,並返回序列的第三個條目的值。
有時我們需要改變一個序列。 假設我們想要改變 series1。 第一種方法使用 change 函式
series1: [1 2] change series1 3 series1 ; == [3 2]
路徑表示法方法
series1: [3 2] series1/1: 1 ; == [1 2]
poke 函式方法
series1: [1 2] poke series1 1 3 ; == [3 2]
如果我們擁有上面提到的變數 i,我們仍然可以使用 change 方法。 詳細內容將在後續章節中介紹。
路徑表示法 方法
series1: [3 2] i: 2 series1/:i: 4
poke 方法
series1: [3 2] i: 2 poke series1 i 4 ; == [3 4]
上面列出的三種資料訪問方法在特殊情況下表現出不同的行為。 讓我們準備 series2 來觀察這些差異
series2: [1 2] f: does [""] poke series2 1 :f
當我們嘗試訪問一個不存在的條目時,會引發錯誤
third series2 ** Script Error: Out of range or past end ** Near: third series2
當我們使用此方法訪問函式時,結果是該函式
type? first series2 ; == function!
當我們嘗試獲取不存在的條目的值時,我們會得到 none 值
series2/3 ; == none
當我們使用路徑方法訪問函式時,該函式將被求值
type? series2/1 ; == string!
當我們嘗試獲取不存在的條目的值時,我們會得到 none 值
pick series2 3 ; == none
當我們使用 pick 方法訪問函式時,結果是該函式
type? pick series2 1 ; == function!
實際上,它並不需要,但這些方法的工作方式不同,如上所示,這就是為什麼我們可以選擇最適合我們任務的方法。
如果我們更喜歡最嚴格的邊界檢查,我們可以選擇 first。
如果我們需要自動評估函式或自動搜尋,我們可以選擇路徑訪問。
如果我們想要一個簡單的訪問方法,不評估函式,我們可以選擇 pick。
一個序列可以包含多個條目。知道特定序列的實際條目數可能很有用。我們可以使用 **length?** 原生函式來查詢它。
length? [1 2 3] ; == 3
另一方面,一個序列也可能沒有任何條目。Rebol 中有一個原生函式 **empty?**,它可以告訴我們一個序列是否“沒有任何條目”。在這種情況下,**length?** 原生函式將返回零。
series3: [] ; == [] empty? series3 ; == true length? series3 ; == 0
有時,我們需要按順序訪問序列,而不是使用索引訪問。Rebol 序列可以在不需要使用任何額外資料型別的情況下,按順序訪問。
為了在一個非空序列中“一次一個條目”地按順序移動,我們可以使用 **next** 原生函式。**next** 原生函式返回一個序列,該序列中第一個條目被“跳過”。原始序列的第二個條目成為新序列的第一個條目。
series8: [1 2 3 4] ; == [1 2 3 4] series9: next series8 ; == [2 3 4]
新序列的第一個、第二個,等等,條目與原始序列的第二個、第三個,等等,條目相同。
series9/1 ; == 2 series8/2 ; == 2 series9/2: 5 ; == [2 5 4] series8/3 ; == 5
在新序列中,我們甚至可以使用被跳過的條目:(僅限 R2)
series9/-1 ; == 1
新序列的長度等於原始序列的長度減一。
length? series8 ; == 3 length? series9 ; == 2
除了每次跳過一個條目,我們還可以使用 **skip** 操作一次跳過多個條目。**skip** 操作的 Rebol 描述,適用於非負的 **offset**。
skip-def: func [
{Skips some places of a series}
series [series!]
offset [integer!] {Can be positive, or zero.}
][
loop offset [
series: next :series
]
:series
]
series8: [1 2 3 4] ; == [1 2 3 4]
series10: skip-def series8 2 ; == [3 4]
如果我們跳過零個條目,我們將獲得原始序列。
series8: [1 2 3 4] ; == [1 2 3 4] same? series8 skip series8 0 ; == true
如果我們跳過序列的所有條目,我們將獲得一個空序列,稱為其尾部。
tail-def: func [
{Returns the tail of a series.}
series [series!]
][
skip :series length? :series
]
tail-def [1 2 3] ; == []
下面是一個函式的定義,該函式確定一個序列是否為尾部。
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
使用 **next** 函式從尾部跳過更多條目,不會有任何效果。
same? next tail [1 2 3] tail [1 2 3] ; == true
我們可以使用 **back** 操作向後跳過一個條目,這相當於 **next** 的反向操作。
series11: tail [1 2 3 4] ; == [] series12: back series11 ; == [4]
讓我們增強 **skip-def**,使其能夠使用負的 **offset** 值。
skip-def: func [
{Skips some places of a series}
series [series!]
offset [integer!] {Can be positive, zero, or negative.}
][
either positive? offset [
loop offset [
series: next :series
]
][
loop negate offset [
series: back :series
]
]
:series
]
series11: tail [1 2 3 4] ; == [] series13: skip-def series11 -2 ; == [3 4]
透過從給定序列中跳過最大可能數量的條目向後,我們獲得的序列稱為給定序列的頭部。任何隨後的 **back** 呼叫都不會再跳過任何條目,並且也會返回頭部。
series14: next [1 2 3] ; == [2 3] head series14 ; == [1 2 3] same? back head series14 head series14 ; == true
我們可以使用上述定義來提供一個 Rebol 函式,用於確定一個序列是否為頭部。
head?-def: func [
{Returns true for a head series.}
series [series!]
][
same? :series back :series
]
head?-def [] ; == true
藉助 **head?**,我們甚至可以提供 **head** 的 Rebol 描述。
head-def: func [
{Returns the head of a series.}
series [series!]
][
while [not head? :series] [series: back :series]
:series
]
“向後”條目的數量加一,可以作為 **index?** 原生函式的結果獲得。
index? next [1 2] ; == 2
讓我們探索一個序列不太自然的屬性。一個序列可以兩次(或更多次)引用一個給定的值。以下函式建立一個塊,該塊恰好兩次引用一個給定的值。
twice: func [value [any-type!]] [
head insert/dup make block! 2 get/any 'value 2
]
讓我們測試我們定義的函式的屬性。
series3: twice 1 ; == [1 1] same? first series3 second series3 ; == true
series4: twice 'o1 ; == [o1 o1] same? first series4 second series4 ; == true
o2: make object! [attribute: "OK"] series5: twice o2 ; == [ ; make object! [ ; attribute: "OK" ; ] ; make object! [ ; attribute: "OK" ; ]] same? first series5 second series5 ; == true o2/attribute: "Surprise?" series5 ; == [ ; make object! [ ; attribute: "Surprise?" ; ] ; make object! [ ; attribute: "Surprise?" ; ]]
類似地,一個值可以被兩個序列引用。
o3: make object! [attribute: "OK"] series6: reduce [o3 1] ; == [ ; make object! [ ; attribute: "OK" ; ] 1]
series7: reduce [o3 2] ; == [ ; make object! [ ; attribute: "OK" ; ] 2] o3/attribute: "Surprised Again?" series6 ; == [ ; make object! [ ; attribute: "Surprised Again?" ; ] 1] series7 ; == [ ; make object! [ ; attribute: "Surprised Again?" ; ] 2]
如果我們想改變一個序列,我們可以使用 **change** 操作。**Change** 可以說改變了序列;它導致受影響的條目引用另一個值。另一方面,**change** 不會影響值。
a: make object! [attribute: "OK"]
series15: twice a ; == [ ; make object! [ ; attribute: "OK" ; ] ; make object! [ ; attribute: "OK" ; ]]
change series15 2 ; == [ ; make object! [ ; attribute: "OK" ; ]]
series15 ; == [2 ; make object! [ ; attribute: "OK" ; ]] probe a ; make object! [ ; attribute: "OK" ; ]
**change** 不會返回原始序列。它跳過受影響的條目,以便於後續更改。
對於任何序列,我們都可以使用 **copy** 操作來建立一個引用相同值的新的序列。序列的副本不是與原序列相同的序列,儘管如此。
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" ; ]]
我們已經看到一個例子,兩個不同的序列條目引用一個 Rebol 值,或者一個例子,兩個不同的序列(例如使用 **skip** 函式準備)共享一些條目。雖然序列條目在 Rebol 中不作為獨立的實體,但我們可以透過指定一個序列和序列中條目的編號來指定一個單獨的條目。因此,我們可以找出兩個指定的條目(引用)是否相同。執行此操作的函式可以在 引用 中找到。
既然我們能夠比較條目,我們就可以說,具有相同資料型別的非空序列如果由相同的條目組成,則它們是相同的。這種方法的問題是,它不適用於空序列,因為在空序列中沒有我們可以用來進行比較的條目。
如果兩個序列既不共享任何條目,也不能使用 **skip** 函式從一個“母”序列中推匯出,我們就稱它們為相互獨立的。在 Rebol 中,這個定義可以寫成如下:
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
觀察:一個序列及其副本是相互獨立的。
**change/only** 的累積屬性:**change/only** 改變其引數序列的引用。從這個描述中可以立即看出,**change/only** 會影響所有共享該引用的序列。
我們可以使用 **insert** 操作建立一個新的引用。
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
我們可以使用 **insert** 來定義空序列的 **same?** 操作的含義。我們說,兩個空序列是相同的,如果使用 **insert** 操作於第一個序列會導致第二個序列也變為非空。
**insert/only** 操作的描述:**insert/only** 在序列中定義一個新的條目,將序列中的條目數量增加一。新插入的條目成為序列的第一個條目,因此序列中其他條目的編號增加一。**insert/only** 導致新建立的條目引用其 **value** 引數。注意,**insert/only** 不會改變任何序列的索引!它跳過插入的條目,以便於後續插入。
**insert/only** 的累積屬性:對於所有依賴於受 **insert/only** 操作影響的序列的序列,都成立:依賴的序列也會增加一個條目,此外,如果依賴的序列的索引低於受影響的序列的索引,則其第一個條目將保持不變,如果依賴的序列的索引等於受影響的序列的索引,則插入後的序列的第一個條目將是新插入的引用,如果依賴的序列的索引高於受影響的序列的索引,則依賴的序列的第一個條目將是原始第一個條目之前的條目。
為了減少序列中的條目數量,我們可以使用 **remove** 和 **clear** 操作。可能每個讀者現在都能使用上述描述的方法來探索這些操作的累積屬性。我將把這些留作讀者的練習。
Rebol 塊是具有快速索引訪問的序列的示例。這意味著,諸如
pick series index
之類的操作具有相同的速度,與以下因素無關:
- 序列的長度
- 索引的值
通常,除 Rebol 列表以外的所有 Rebol 序列都屬於這種型別。
- 索引訪問的速度(pick、poke、skip、at、length?、index?、offset? 等)。
- 一般來說,刪除/插入操作是“慢”的,執行刪除/插入操作所需的時間通常與序列的長度成正比。
- 在任何插入/刪除操作之後,所有相關序列的索引保持不變,這意味著在插入或刪除操作之後,序列實際上引用的是與操作之前不同的條目。
s: [1 2 3 4 5 6] t: skip s 2 ; == [3 4 5 6] remove s t ; == [4 5 6]
- 由於上述屬性,甚至有可能獲得過去尾部序列,即索引實際上在給定序列中“不可用”的序列。
如前所述,只有一種 Rebol 序列型別具有此屬性,那就是 list! 資料型別。
- 刪除/插入操作很快。
- 在插入操作之後,所有 Rebol 列表都保持“不受影響”,因為它們指向執行插入操作之前指向的條目。
- 在刪除操作之後,所有 Rebol 列表(除了直接受影響的那些,因為它們第一個條目被刪除)都保持“不受影響”,因為它們指向執行刪除操作之前指向的條目。
- 任何列表都不會變成過去尾部列表。
- 由於 Rebol 列表在 R2 中的實現方式,刪除列表的第一個元素會導致該列表變成一個“具有無效(已刪除)位置的列表”;但是請參見下面的我的列表原型,它在沒有顯著降低任何操作速度的情況下解決了這個問題;該原型調整了此類列表,使其指向第一個後續有效條目,而不是已刪除的條目。
- 一般來說,索引操作是“慢”的;執行此類操作所需的時間通常與列表的長度成正比。
由於列表在 R3 中不存在,並且由於上述已刪除條目的問題,我決定實現一個可以改善這種情況的列表原型。
use [] [
list-proto: make object! [
; the position of a list is not an integer index,
; it is a list entry
position: none
; to have fast tail tail? head head?
; we need to know the position of the list tail
tail: none
]
list-entry-proto: make object! [
; every list entry refers to a value
; the tail entry always refers to NONE
value: none
; for every list entry we want to know,
; where the previous entry is
; for convenience we define,
; that the previous entry of a head entry
; is the tail entry
; to be able to detect the deleted entries
; we define, that the previous entry
; of a deleted entry is NONE
previous: none
; for every list entry we want to know,
; where the subsequent entry is
; for convenience we define,
; that the subsequent entry of the tail entry
; is the head entry
subsequent: none
]
set 'make-empty-list func [
{create an empty list}
] [
make list-proto [
position: tail: make list-entry-proto []
; head entry is the tail entry for empty lists
tail/subsequent: tail/previous: tail
]
]
set 'tail-s func [
{return the tail of a list}
list [object!]
] [
make list-proto [position: tail: list/tail]
]
set 'head-s func [
{return the head of a list}
list [object!]
] [
make list-proto [
tail: list/tail
position: tail/subsequent
]
]
set 'last-s func [
{returns the last value of a list}
list [object!]
] [
return get/any in list/tail/previous 'value
]
adjust-position: func [
{this function adjusts the list position to not refer to a removed entry}
list [object!] {the list to check/adjust}
/local current valid subsequent
] [
; note:
; even though this function uses two while cycles,
; it is implemented so,
; that its running time is insignificant
; explanation:
; for the adjust-position call to take O(n) time
; it is necessary to perform n removes,
; and after the adjustment,
; all subsequent calls to the adjust-position function
; become O(1) again
; this means, that the adjust-position call adds just
; O(1) time to every remove call,
; in addition to the O(1) time needed for every adjust-position call
; when the list position is valid
; we look for the first valid entry
current: list/position
while [
; test, if the current entry is a removed entry
none? current/previous
] [current: current/subsequent]
valid: current
; now we "clean" the removed entries
; to not have to traverse them any more
current: list/position
while [
; test, if the current entry is a removed entry
none? current/previous
] [
subsequent: current/subsequent
; doing the cleanup
current/subsequent: valid
current: subsequent
]
; adjust the list position to be valid
list/position: valid
]
set 'head?-s func [
{returns TRUE if at head}
list [object!]
] [
adjust-position list
list/position = list/tail/subsequent
]
set 'tail?-s func [
{returns TRUE if at tail}
list [object!]
] [
adjust-position list
list/position = list/tail
]
set 'index?-s func [
{returns the index of a list}
list [object!]
/local current index
] [
adjust-position list
current: list/position
index: 1
while [
; the current entry is not head
current/previous <> list/tail
] [
index: index + 1
current: current/previous
]
index
]
set 'length?-s func [
{returns the length of a list}
list [object!]
/local current length
] [
adjust-position list
current: list/position
length: 0
while [
; the current entry is not tail
current <> list/tail
] [
length: length + 1
current: current/subsequent
]
length
]
set 'skip-s func [
{returns the list forward or backward from the current position}
list [object!]
offset [integer!]
/local current
] [
adjust-position list
current: list/position
either negative? offset [
; traversing the list towards its head
while [
all [
; not at the head?
current/previous <> list/tail
offset < 0
]
] [
current: current/previous
offset: offset + 1
]
] [
; traversing the list towards its tail
while [
all [
; not at the tail?
current <> list/tail
offset > 0
]
] [
current: current/subsequent
offset: offset - 1
]
]
make list-proto [
position: current
tail: list/tail
]
]
set 'first-s func [
{returns the first value of a list}
list [object!]
] [
adjust-position list
return get/any in list/position 'value
]
set 'remove-s func [
{removes an element from a list}
list [object!]
/local current
] [
adjust-position list
current: list/position
either current = list/tail [
; no removal needed, just return the argument
list
] [
; adjust links
current/previous/subsequent: current/subsequent
current/subsequent/previous: current/previous
; mark the current element as removed
current/previous: none
; return the next position
make list-proto [
position: current/subsequent
tail: list/tail
]
]
]
set 'insert-only func [
{insert one value into a list}
list [object!]
value [any-type!]
] [
adjust-position list
make list-proto [
; create a new list entry
position: make list-entry-proto [
previous: list/position/previous
subsequent: list/position
]
error? set/any in position 'value get/any 'value
; adjust links
position/previous/subsequent: position
position/subsequent/previous: position
tail: list/tail
]
]
set 'change-only func [
{change a value in a list}
list [object!]
value [any-type!]
/local new-entry
] [
adjust-position list
if list/position = list/tail [
; we are at tail, change is not possible
do make error! "out of range"
; (alternatively, we may insert a new entry, if preferred)
]
error? set/any in list/position 'value get/any 'value
; return the list at the next position
make list-proto [
position: list/position/subsequent
tail: list/tail
]
]
]