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 ] ] ]