【问题标题】:Is there a overall explanation about definitional scoping in Rebol and Red是否有关于 Rebol 和 Red 中定义范围的总体解释
【发布时间】:2014-03-24 17:16:09
【问题描述】:

REBOL/Core Users GuideWhat is Red,我了解到Rebol 和Red 都使用定义范围

从指南中我知道它是静态作用域的一种形式,“变量的作用域是在定义其上下文时确定的”,也叫runtime lexical scoping,是@ 987654324@.

我知道在 com-sci 中有两种形式的作用域:词法作用域(静态作用域)和动态作用域。这个定义范围让我很困惑。

那么什么是定义范围?

【问题讨论】:

    标签: scope rebol red


    【解决方案1】:

    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 使用了外部作用域的afunc-2 使用的a 来自内部作用域,而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 没有做任何这些。特别是它不是在运行时编译的,而是构建的。你认为的代码实际上是数据、字块、数字等。这些词是数据结构,其中有一个称为“绑定”的指针。

    当第一次加载该脚本时,脚本中的所有单词都被添加到脚本的环境对象中(我们不恰当地将其称为“上下文”,尽管它不是)。在收集单词的同时,脚本数据发生了变化。在脚本的“上下文”中找到的任何单词都与“上下文”或“绑定”相关联。这些绑定意味着您可以只跟随一个链接并到达存储该单词值的对象。真的很快。

    然后,一旦完成,我们就开始运行脚本。然后我们到了这一点:func [] [a]。这并不是真正的声明,而是对名为func 的函数的调用,该函数采用规范块和代码块并使用它们来构建函数。该函数也有自己的环境对象,但在函数的规范中声明了单词。在这种情况下,规范中没有单词,所以它是一个空对象。然后将代码块绑定到该对象。但在这种情况下,该对象中没有 a,因此对 a 没有任何操作,它保留了之前绑定时已有的绑定

    context [...] 调用也是如此 - 是的,这是对一个名为 context 的函数的调用,它通过调用 make object! 来构建一个对象。 context 函数获取一个数据块,它搜索集合词(那些带有尾随冒号的东西,比如a:),然后用这些词构建一个对象,然后将所有词绑定到该块和对象中单词的所有嵌套块,在本例中为afunc-2func-3。这意味着该代码块中的a 的绑定已更改,改为指向该对象。

    func-2 被定义时,a 在其代码块中的绑定不会被覆盖。当func-3 被定义时,它的规范中有一个a,所以a: 的绑定被覆盖了。

    所有这一切的有趣之处在于根本没有任何作用域。第一个a:func-1 代码体中的a 只绑定一次,因此它们保持第一个绑定。 inner 的代码块中的 a:func-2 中的 a 绑定了两次,因此它们保留了第二次绑定。 func-3的代码中的a:被绑定了3次,所以也保持最后一次绑定。这不是作用域,它只是绑定代码,然后再次绑定更小的代码,依此类推,直到完成。

    每一轮绑定都由一个正在“定义”某些东西(实际上是在构建它)的函数执行,然后当该代码运行并调用其他定义了其他内容的函数时,这些函数会对其进行另一轮绑定代码的子集。这就是我们称之为“定义范围”的原因;虽然它实际上不是作用域,但它是 Rebol 中作用域的目的,它与词法作用域的行为非常接近,乍一看你无法区分。

    当您意识到这些绑定是直接的时,情况就真的变得不同了,您可以更改它们(有点,您可以使用相同的名称和不同的绑定创建新单词)。那些定义函数调用的同一个函数,你可以自己调用:它被命名为bind。使用bind,您可以打破范围界定的错觉,并让单词绑定到您可以访问的任何对象。你可以用bind 做精彩的技巧,甚至可以自己定义函数。很有趣!

    对于 Red,Red 是可编译的,但它还包括类似 Rebol 的解释器、绑定和所有好东西。当它使用解释器定义事物时,它也会定义范围。

    这是否有助于使事情更清楚?

    【讨论】:

    • 感谢 BrianH 的精彩解释。但是使用“make object![]”或“context”而不是“object []”会不会更好,所以它也可以与 Rebol2 兼容?
    • 该死,我认为它已经进入了向后移植的函数。我将它切换到context,并将关于它是一个函数的评论移到讨论func
    • 这是对这个话题的最清晰的描述!感谢 BrianH 的努力。它认为在同一页面上提及 Ladislav 的知名、更详细但更难消化的 rebol.net/wiki/Bindology 文章可能有助于初学者更容易发现您的解释。
    【解决方案2】:

    这是一个老问题,@BrianH 在这里的回答对机制非常透彻。但我想我会给出一个稍微不同的重点,作为一个“故事”。

    在 Rebol 中,有一类称为 words 的类型。这些本质上是符号,因此它们的字符串内容被扫描并进入符号表。因此,"FOO" 将是一个字符串,而<FOO> 将是字符串的另一种“风味”,称为标签……FOO'FOOFOO::FOO 都是各种“风味”具有相同符号 ID 的单词。 (分别是一个“单词”、一个“lit-word”、一个“set-word”和一个“get-word”。)

    被折叠成一个符号使得加载后无法修改单词的名称。与每个都有自己的数据并且是可变的字符串相比,它们被卡住了:

    >> append "foo" "bar"
    == "foobar"
    
    >> append 'foo 'bar
    ** Script error: append does not allow word! for its series argument
    

    不变性有一个优势,作为一个符号,它可以快速将一个词与另一个词进行比较。但是还有另一个难题:一个单词的每个实例都可以有一个不可见的属性,称为 binding。该绑定让它“指向”一个称为 context 的键/值实体,其中可以读取或写入值。

    注意:与@BrianH 不同,我认为将此类绑定目标称为“上下文”并不是那么糟糕——至少我今天不这么认为。稍后再问我,如果有新的证据出现,我可能会改变主意。可以说它是一个类似对象的东西,但并不总是一个对象……例如,它可能是对堆栈上函数框架的引用。

    谁将一个词带入系统,谁就能率先说出它所绑定的上下文。很多时候都是加载,所以如果你说load "[foo: baz :bar]" 并取回三字块[foo: baz :bar] 他们将被绑定到“用户上下文”,并回退到“系统上下文”。

    遵循绑定就是一切的运作方式,每个单词的“风味”都有不同的作用。

    >> 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 是一个“函数生成器”,它接受一个规范和一个你给它的主体,它有能力获取主体的“原始物质”及其绑定并覆盖它决定的任何一个。或许很怪异,但看看这个:

    >> x: 10
    
    >> foo: func [x] [
        print x
        x: 20
        print x
    ]
    
    >> foo 304
    304
    20
    
    >> print x
    10
    

    FUNC 收到了两个块,一个代表参数列表,第二个代表正文。当它得到主体时,prints 都绑定到本机打印功能 (在这种情况下 - 重要的是要指出,当您从控制台以外的地方获取材料时,它们都可以以不同的方式绑定!)x 绑定到用户上下文(在这种情况下),它的值是 10。如果 FUNC 没有做任何事情来改变这种情况,事情就会保持这种状态。

    但它把图片放在一起并决定,由于参数列表中有一个 x,它会查看正文并覆盖带有 x 符号 ID 的单词和一个新的绑定...本地的功能。这是它没有用x: 20 覆盖全局的唯一原因。如果您在规范 FUNC 中省略了 [x],则不会执行任何操作,并且会被覆盖。

    定义链中的每个部分在传递事物之前都有机会。因此定义范围

    有趣的事实:因为如果你不向 FUNC 的规范提供参数,它不会重新绑定正文中的任何内容,这导致了错误的印象,即 “Rebol 中的所有内容都在全局范围内”时间>。但这根本不是真的,因为正如@BrianH 所说:“Rebol 实际上根本没有作用域(...)Rebol 伪造它。” 事实上,这就是 FUNCTION(与 FUNC 相对)确实——它在正文中寻找像 x: 这样的集合词,当它看到它们时将它们添加到本地框架并绑定到它们。效果看起来像局部范围,但又不是!

    如果想象这些带有不可见指针的符号在四处移动听起来有点像 Rube-Goldberg-esque,那是因为它是。对我个人而言,了不起的事情是它完全有效……而且我看到人们用它来做特技,你不会凭直觉认为可以使用这么简单的技巧。

    恰当的例子:非常有用的 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-eachlength? 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 设置为一个函数,该函数在调用时将数据添加到聚合器。

    在这里,我们通过 /ONLY 开关看到了 KEEP 函数在将块拼接到收集的输出中的多功能性(只有当我们看到长度为 3 的项目时,调用者才选择不拼接)。但这只是表面问题。它只是一个非常强大的语言功能——用户事后添加的——源自如此少的代码,几乎令人恐惧。当然还有更多的故事。

    我在这里添加了一个答案,因为我已经为定义范围填写了一个重要的缺失链接,这个问题被称为“定义范围的返回”:

    https://codereview.stackexchange.com/questions/109443/definitional-returns-solved-mostly

    这就是为什么 &lt;with&gt; return 在规范中与 KEEP 并排的原因。它在那里是因为 COLLECT 试图告诉 FUNC 它想“使用它的服务”作为代码的绑定器和运行器。但身体已经由其他人在其他地方创作。因此,如果它有一个 RETURN,那么这个 RETURN 已经知道要返回到哪里。 FUNC 只是为了“重新调整”keep 的范围,但保留任何回报而不是添加自己的回报。因此:

    >> foo: func [x] [
         collect [
             if x = 10 [return "didn't collect"]
             keep x
             keep 20
         ]
    ]
    
    >> foo 304
    == [304 20]
    
    >> foo 10
    == "didn't collect"
    

    &lt;with&gt; return 使 COLLECT 能够足够聪明地知道在 FOO 的体内,它不希望返回反弹,因此它认为从参数只是 [keep] 的函数返回。

    还有一点关于定义范围的“为什么”,而不是“什么”。 :-)

    【讨论】:

    • @klausnrooster 谢谢,虽然我可能应该更新这个,但从那以后我发现了一些其他的东西。但你也可能会觉得Rebol vs. Lisp Macros很有趣...
    • 哈,“this”这个词与该页面相关联。我应该更明确。
    • @klausnrooster 啊,很难看到链接(并且不显示在通知中)!好吧,我也写了... :-)
    • 不要试图修复从根本上被破坏的东西,这就是我要说的。
    • @MaartenBodewes 绘画是否从根本上被破坏了,我们都应该使用SolidWorks,捕获每个方程和对称性,这样就不会失去同步?我想没有人应该把他们的生命浪费在画廊或文学上,那么。或者也许每个从事艺术的人都应该认为 M.C. Escher 是它的巅峰之作......和/或在 Haskell 中做所有事情?编程是一种艺术形式,学习更高层次的编程很像学习如何打破其他艺术的规则。 Maybe someday you'll get there. :-P
    【解决方案3】:

    我的理解是:

    Rebol 是静态作用域的

    但是,

    问题不是“Rebol 采用什么范围?”,而是“何时确定 Rebol 范围,何时编译 Rebol 程序?”。

    Rebol 具有静态作用域,但具有动态编译。

    我们习惯了有一个编译时间和一个运行时间。

    Rebol 有多个编译时间。

    Rebol 代码的编译取决于编译时存在的上下文。

    Rebol 代码在不同的时间、不同的上下文中编译。这意味着 Rebol 函数可能在不同的时间以不同的方式编译。

    【讨论】:

      猜你喜欢
      • 2023-03-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-30
      • 1970-01-01
      • 1970-01-01
      • 2018-07-05
      相关资源
      最近更新 更多