从REBOL/Core Users Guide和What is Red,我了解到Rebol和Red都使用定义范围。
从指南中我知道它是静态作用域的一种形式,“变量的作用域在定义其上下文时确定”,也称为runtime lexical scoping,并且是a dynamic form of static scoping that depends on context definitions。
我知道在com-sci中,有两种形式的范围:词法范围(静态范围)和动态范围。这个定义范围让我很困惑。
那么什么是定义范围?
答案 0 :(得分:27)
Rebol实际上根本没有范围。
我们来看看这段代码:
rebol []
a: 1
func-1: func [] [a]
inner: context [
a: 2
func-2: func [] [a]
func-3: func [/local a] [a: 3 func-1]
]
因此,加载该代码后,如果Rebol有词法范围,这就是您所看到的:
>> reduce [func-1 inner/func-2 inner/func-3]
== [1 2 1]
这是因为func-1
使用了外部作用域中的a
,a
使用的func-2
来自内部作用域,func-3
调用func-1
,仍然使用a
来自定义它的外部作用域func-3
。
如果Rebol有动态范围,这就是你所看到的:
>> reduce [func-1 inner/func-2 inner/func-3]
== [1 2 3]
这是因为func-3
重新定义了a
,然后调用了func-1
,它只使用a
的最新有效定义。
现在对于Rebol,你得到了第一个结果。但是Rebol没有词法范围。那为什么呢?
Rebol假装它。这是它的工作原理。
在编译语言中,您有范围。当编译器遍历文件时,它会跟踪当前作用域,然后当它看到嵌套作用域成为当前作用域时。对于词法作用域,编译器保留对外部作用域的引用,然后通过跟随外部作用域的链接查找当前作用域中未定义的单词,直到找到该单词,或者不对其进行查找。吨。动态范围的语言做类似的事情,但在运行时,调高堆栈。
Rebol没有做到这一点;特别是它不是在运行时编译的,而是它的构建。您认为代码实际上是数据,单词块,数字等。单词是在其中具有指针的数据结构,称为"绑定"。
首次加载该脚本时,脚本中的所有单词都会添加到脚本的环境对象中(我们不恰当地称之为" context",尽管它不是)。在收集单词时,脚本数据会发生变化。脚本中的任何单词" context"链接到" context"或" bound"。这些绑定意味着您可以只跟随一个链接并到达存储该单词值的对象。它真的很快。
然后,一旦完成,我们就开始运行脚本了。然后我们谈谈这一点:func [] [a]
。这不是一个真正的声明,它是对名为func
的函数的调用,该函数接受spec块和代码块并使用它们来构建函数。该函数也获得了自己的环境对象,但在函数规范中声明了单词。在这种情况下,规范中没有单词,因此它是一个空对象。然后代码块绑定到该对象。但是在这种情况下,该对象中没有a
,因此a
没有任何操作,它保留了它在绑定之前已经具有的绑定。 / p>
同样适用于context [...]
调用 - 是的,这是对不恰当地命名为context
的函数的调用,该函数通过调用make object!
来构建对象。 context
函数接受一个数据块,它搜索set-words(带有结尾冒号的那些东西,如a:
),然后构建一个带有这些单词的对象,然后它绑定所有该块中的单词以及对象中所有单词的嵌套块,在本例中为a
,func-2
和func-3
。这意味着该代码块中的a
的绑定已更改,而是指向该对象。
定义func-2
时,不会覆盖其代码块中a
的绑定。定义func-3
后,其规范中包含a
,因此a:
已覆盖其绑定。
所有这一切的有趣之处在于根本没有任何范围。 a:
代码正文中的第一个a
和func-1
仅绑定一次,因此它们保留了第一个绑定。 a:
代码块中的inner
和a
中的func-2
被绑定两次,因此他们保留第二个绑定。 a:
代码中的func-3
绑定了三次,因此它也保留了最后一次绑定。它不是范围,它只是被绑定的代码,然后再次绑定较小的代码,依此类推,直到完成为止。
每轮绑定都是通过&#34;定义&#34;某些东西(实际上是构建它),然后当代码运行并调用定义其他东西的其他函数时,这些函数会对其小部分代码执行另一轮绑定。这就是为什么我们称之为&#34;定义范围&#34 ;;虽然它确实没有范围,但它正是Rebol中确定范围的目的,它与词汇范围的行为非常接近,乍看之下你无法区分它。< / p>
当你意识到这些绑定是直接的,并且你可以改变它们时,它真的变得不同了(有点,你可以创建具有相同名称和不同绑定的新单词)。这些定义函数调用的功能相同,您可以自称:它名为bind
。使用bind
,您可以打破范围界定的错觉,并创建绑定到您可以访问的任何对象的单词。您可以使用bind
做一些精彩的技巧,甚至可以创建自己的定义函数。这很有趣!
至于Red,Red是可编辑的,但它还包括一个类似Rebol的解释器,绑定和所有的好东西。当它用解释器定义事物时,它也会定义范围。
这有助于使事情更清楚吗?
答案 1 :(得分:13)
这是一个老问题,@ BrianH的答案在机制上非常彻底。但我想我会给一个稍微不同的焦点,作为一个更多的故事&#34;。
在Rebol中,有一类称为单词的类型。这些本质上是符号,因此扫描它们的字符串内容并将它们放入符号表中。因此,"FOO"
将是一个字符串,而<FOO>
将是另一个&#34;味道&#34;被称为标签的字符串... FOO
,'FOO
,FOO:
和:FOO
都是各种各样的&#34;风味&#34;具有相同符号ID的单词。 (A&#34;字&#34;,&#34; lit-word&#34;,&#34; set-word&#34;和&#34; get-word&#34;分别。)
折叠为符号后,无法在加载后修改单词的名称。与每个都有自己的数据且可变的字符串相比,它们会卡住:
>> append "foo" "bar"
== "foobar"
>> append 'foo 'bar
** Script error: append does not allow word! for its series argument
不变性具有优势,因为作为符号,将一个词与另一个词进行比较是很快的。但是还有另外一个难题:一个单词的每个实例都可以选择在其中有一个名为绑定的不可见属性。那个绑定让它指向&#34;称为上下文的键/值实体,其中可以读取或写入值。
注意:与@BrianH不同,我不认为调用此类绑定目标&#34; contexts&#34;就是这么糟糕 - 至少我今天不会这么想。稍后问我,如果有新证据出现,我可能会改变主意。可以说它是一个像对象一样的东西,但并不总是一个对象......例如,它可能是对堆栈中函数框架的引用。
无论谁在系统中加入一个单词,都会首先说明它所绑定的上下文。很多时候它都会加载,所以如果你说load "[foo: baz :bar]"
并找回3字块[foo: baz :bar]
,它们将被绑定到&#34;用户上下文&#34;,回退到&#34;系统环境&#34;。
绑定之后是一切如何运作,以及每个&#34;味道&#34;这个词做了不同的事。
>> print "word pointing to function runs it"
word pointing to function runs it
>> probe :print "get-word pointing to function gets it"
make native! [[
"Outputs a value followed by a line break."
value [any-type!] "The value to print"
]]
== "get-word pointing to function gets it"
注意:第二种情况并没有打印出该字符串。它探测了函数规范,然后字符串只是评估中的最后一件事,所以它对它进行了评估。
但是一旦你手上拿着一个包含文字的数据块,绑定就是任何人的游戏。只要上下文中包含单词的符号,您就可以将该单词重新定位到该上下文。 (假设该块未受到保护或锁定以防止修改......)
这种重新结合机会的连锁链是重要的一点。由于FUNC是一个&#34;函数发生器&#34;这需要一个规范和你给它的身体,它有能力采取&#34;原始物质&#34;身体的绑定和覆盖它决定的任何一个。也许是怪异的,但看看这个:
>> x: 10
>> foo: func [x] [
print x
x: 20
print x
]
>> foo 304
304
20
>> print x
10
FUNC收到了两个块,一个代表参数列表,另一个代表一个体。当它到达正文时,print
都被绑定到原生打印功能(在这种情况下 - 并且指出当你从以下材料获取材料时非常重要除了控制台之外的其他地方,每个地方都有不同的约束!)。 x
绑定到用户上下文(在这种情况下),它持有值10.如果FUNC没有做任何改变这种情况的事情,事情会保持这种状态。
但是它把图片放在一起并决定,因为参数列表中有一个x,它会通过正文看起来,覆盖带有x的符号ID的单词带有新的绑定...功能的本地。这是它没有用x: 20
覆盖全局的唯一原因。如果你省略了规范中的[x],FUNC就不会做任何事情,而且它会被覆盖。
定义链中的每个部分在传递之前都有机会。因此定义范围。
有趣的事实:因为如果你没有为FUNC的规格提供参数,它就不会重新绑定身体中的任何东西,这会导致错误的印象&#34; Rebol中的一切在全球范围内&#34; 。但这根本不是真的,因为正如@BrianH所说:&#34; Rebol实际上根本没有确定范围(...)Rebol假装它。&#34; 实际上这就是FUNCTION(与FUNC相反)的作用 - 它会在体内寻找像 x:这样的设定词,当它看到它们时会将它们添加到本地帧中与他们绑定。效果看起来像本地范围,但同样,它不是!
如果听起来有点Rube-Goldberg-esque想象这些带有隐形指针的符号被拖曳,那是因为它是。就个人而言,非凡的事情就是它完全有效......而且我已经看到人们用它来拉动特技,你不会直觉地认为这样一个简单的技巧可以用来做。 / p>
例证:疯狂有用的COLLECT和KEEP(Ren-C version):
collect: func [
{Evaluates a block, storing values via KEEP function,
and returns block of collected values.}
body [block!] "Block to evaluate"
/into {Insert into a buffer instead
(returns position after insert)}
output [any-series!] "The buffer series (modified)"
][
unless output [output: make block! 16]
eval func [keep <with> return] body func [
value [<opt> any-value!] /only
][
output: insert/:only output :value
:value
]
either into [output] [head output]
]
这个不起眼的工具扩展了以下风格的语言(同样,Ren-C版本......在R3-Alpha或Rebol2替代foreach
for for-each
和length?
for length of
)
>> collect [
keep 10
for-each item [a [b c] [d e f]] [
either all [
block? item
3 = length of item
][
keep/only item
][
keep item
]
]
]
== [10 a b c [d e f]]
通过我上面提到的内容,最好地理解定义范围的技巧。 FUNC只会覆盖参数列表中事物的绑定,并保持体内其他所有内容不变。所以会发生这样的情况,它会将您传递给COLLECT的主体用作新函数的主体,并覆盖KEEP的任何绑定。然后,它将KEEP设置为一个函数,在调用时将数据添加到聚合器。
在这里,我们看到KEEP功能通过/ ONLY开关将拼接块拼接到收集的输出中的多功能性(只有当我们看到长度为3的项目时,呼叫者选择不拼接)< / em>的。但这只是表面上的问题。它只是一个非常强大的语言功能 - 用户在事后添加 - 源于如此少的代码,它几乎是可怕的。当然还有更多的故事。
我在这里添加了一个答案,因为已经填写了定义范围界定的关键缺失链接,这个问题被称为&#34;定义范围的返回&#34;:
https://codereview.stackexchange.com/questions/109443/definitional-returns-solved-mostly
这就是<with> return
与规范中的KEEP并列的原因。它就在那里因为COLLECT试图告诉FUNC它想要&#34;使用它的服务&#34;作为代码的粘合剂和流动者。但是身体已经由其他人在其他地方创作。因此,如果它有一个RETURN,那么RETURN已经知道返回的位置。 FUNC只是为了重新定位&#34;保持,但留下任何回报,而不是添加自己的。因此:
>> foo: func [x] [
collect [
if x = 10 [return "didn't collect"]
keep x
keep 20
]
]
>> foo 304
== [304 20]
>> foo 10
== "didn't collect"
<with> return
使得COLLECT能够足够聪明地知道在FOO的体内,它不希望返回反弹,以便它认为从参数的函数返回只是[保持]而已。
还有一些关于&#34;为什么&#34;定义范围界定,而不仅仅是&#34;什么&#34;。 : - )
答案 2 :(得分:0)
我的理解是:
Rebol 是静态作用域
但是,
问题不是“Rebol 使用什么范围?”,而是“何时确定 Rebol 范围,何时编译 Rebol 程序?”。
Rebol 具有静态作用域,但具有动态编译。
我们习惯了一种编译时间和一种运行时间。
Rebol 有多个编译时间。
Rebol 代码的编译取决于编译时存在的上下文。
Rebol 代码在不同的时间、不同的上下文中编译。这意味着 Rebol 函数可能在不同时间编译不同。