【问题标题】:Why does return/redo evaluate result functions in the calling context, but block results are not evaluated?为什么 return/redo 在调用上下文中评估结果函数,但不评估块结果?
【发布时间】:2013-01-23 12:44:39
【问题描述】:

昨晚我了解了 /redo 选项,当您从函数中 return 时。它允许您返回 另一个 函数,然后在调用站点调用该函数并从同一位置重新调用评估器

>> foo: func [a] [(print a) (return/redo (func [b] [print b + 10]))] 

>> foo "Hello" 10
Hello
20

尽管foo 是一个只接受一个参数的函数,它现在就像一个接受两个参数的函数。否则,这样的事情会要求调用者知道您正在返回一个函数,并且该调用者必须手动对其使用 do 评估器。

因此没有return/redo,你会得到:

>> foo: func [a] [(print a) (return (func [b] [print b + 10]))] 

>> foo "Hello" 10
Hello
== 10

foo 消耗了它的一个参数并按值返回了一个函数(它没有被调用,因此解释器继续前进)。然后表达式计算为 10。如果 return/redo 不存在,您必须编写:

>> do foo "Hello" 10
Hello
20

这使调用者不必知道(或关心)您是否选择返回要执行的函数。并且很酷,因为您可以执行尾调用优化或为返回功能本身编写包装器之类的事情。这是return 的一个变体,它打印一条消息但仍退出函数并提供结果:

>> myreturn: func [] [(print "Leaving...") (return/redo :return)]

>> foo: func [num] [myreturn num + 10]

>> foo 10
Leaving...
== 20

但函数并不是do 中唯一具有行为的东西。因此,如果这是“在调用点消除对 DO 的需求”的一般模式,那么为什么不打印任何内容呢?

>> test: func [] [return/redo [print "test"]]

>> test 
== [print "test"]

它只是按值返回块,就像正常返回一样。它不应该打印出“测试”吗?这就是do 会......呃,用它来做:

>> do [print "test"]
test

【问题讨论】:

    标签: return rebol tail-call-optimization trampolines rebol3


    【解决方案1】:

    简短的回答是因为通常不需要在调用点评估块,因为 Rebol 中的块不带参数,所以 大多数情况下 在哪里评估它们并不重要。但是,“大部分”可能需要一些解释......

    归结为 Rebol 的两个有趣的特性:静态绑定,以及函数的 do 是如何工作的。

    静态绑定和作用域

    Rebol 没有范围词绑定,它有静态直接词绑定。有时看起来我们有词法作用域,但我们确实通过在每次构建新的“作用域”代码块时更新静态绑定来伪造它。我们还可以随时手动重新绑定单词。

    在这种情况下,这对我们来说意味着,一旦一个块存在,它的绑定和值是静态的 - 它们不受该块的物理位置或正在评估的位置的影响。

    然而,这就是它变得棘手的地方,函数上下文很奇怪。虽然绑定到函数上下文的单词绑定是静态的,但分配给这些单词的值集动态范围的。这是如何在 Rebol 中评估代码的副作用:什么是其他语言中的语言语句是 Rebol 中的函数,例如,对 if 的调用实际上将一个数据块传递给 if 函数 @987654325 @ 然后传递给do。这意味着,当一个函数正在运行时,do 必须从最近一次调用尚未返回的函数的调用帧中查找其单词的值。

    这确实意味着,如果您调用一个函数并返回一个代码块,其中单词绑定到其上下文,则在函数返回后评估该块将失败。但是,如果您的函数调用 itself 并且 that 调用返回一个代码块,其中的单词绑定在它上面,那么在函数返回之前评估 that 块将使它在您的函数的当前调用的调用框架中查找这些单词。

    无论您是do 还是return/redo,这都是一样的,并且也会影响内部功能。让我演示一下:

    在函数返回后计算的函数返回代码,引用一个函数字:

    >> a: 10 do do has [a] [a: 20 [a]]
    ** Script error: a word is not bound to a context
    ** Where: do
    ** Near: do do has [a] [a: 20 [a]]
    

    相同,但使用 return/redo 和函数中的代码:

    >> a: 10 do has [a] [a: 20 return/redo does [a]]
    ** Script error: a word is not bound to a context
    ** Where: function!
    ** Near: [a: 20 return/redo does [a]]
    

    代码do 版本,但在对同一函数的外部调用中:

    >> do f: function [x] [a: 10 either zero? x [do f 1] [a: 20 [a]]] 0
    == 10
    

    相同,但使用 return/redo 和函数中的代码:

    >> do f: function [x] [a: 10 either zero? x [f 1] [a: 20 return/redo does [a]]] 0
    == 10
    

    因此,简而言之,对于块,在定义块之外的其他地方执行块通常没有优势,如果您愿意,可以更容易地使用另一个调用 do 来代替。自调用递归函数需要返回要在同一函数的外部调用中执行的代码,这是一种极其罕见的代码模式,我从未见过在 Rebol 代码中使用过。

    可以更改return/redo,以便它也可以处理块,但可能不值得增加return/redo 的开销来添加仅在极少数情况下有用并且已经具有更好的功能去do它的方式。

    然而,这带来了一个有趣的观点:如果你不需要 return/redo 用于块,因为 do 做同样的工作,那么函数不同样适用吗?为什么我们需要return/redo

    函数的 DO 工作原理

    基本上,我们有return/redo,因为它使用的代码与我们用于实现函数的do 的代码完全相同。你可能没有意识到,do 的函数确实很不寻常。

    在大多数可以调用函数值的编程语言中,您必须将参数作为一个完整的集合传递给函数,类似于 R3 的 apply 函数的工作原理。常规 Rebol 函数调用会导致使用未知提前评估规则对其参数进行一些提前未知数量的附加评估。评估器在运行时计算出这些评估规则,并将评估结果传递给函数。函数本身不处理其参数的评估,甚至不一定知道如何评估这些参数。

    但是,当您明确地do 一个函数值时,这意味着将函数值传递给对 另一个 函数的调用,即一个名为 do常规 函数,然后神奇地导致评估附加参数,甚至根本没有传递给 do 函数

    这不是魔法,它是return/redo。函数do 的工作方式是它以常规快捷方式返回值返回对函数的引用,快捷方式返回值中带有一个标志,告诉解释器调用 @987654353 @ 评估返回的函数,就好像它在代码中被调用一样。这基本上就是所谓的蹦床。

    这里是 Rebol 的另一个有趣特性:从函数中快捷返回值的能力内置于求值器中,但它实际上并没有使用 return 函数来执行此操作。您从 Rebol 代码中看到的所有函数都是内部内容的包装器,甚至是 returndo。我们调用的return 函数只是生成这些快捷方式返回值之一并返回它;其余的由评估者完成。

    所以在这种情况下,真正发生的事情是,一直以来我们都有代码可以完成 return/redo 内部所做的事情,但 Carl 决定在我们的 return 函数中添加一个选项来设置该标志,即使内部代码不需要return 这样做,因为内部代码调用内部函数。然后他没有告诉任何人他正在使该选项在外部可用,或者为什么,或者它做了什么(我想你不能提到所有事情;谁有时间?)。根据与 Carl 的对话以及我们一直在修复的一些错误,我怀疑 R2 以不同方式处理函数的 do,这会使 return/redo 不可能。

    这确实意味着return/redo 的处理非常彻底地面向函数评估,因为这就是它存在的全部原因。向它添加任何开销都会给函数的do 增加开销,我们使用它一个lot。可能不值得将其扩展到区块,因为我们获得的收益很少,而且我们很少获得任何好处。

    不过,对于一个函数的return/redo,我们越想它,它似乎就越有用。在过去的中,我们想出了各种可以实现的技巧。蹦床很有用

    【讨论】:

    • 感谢您通过查看代码来回答这个问题的逆向工程工作。我经常在这里回答有关开源问题的问题well, it's open source, go look。现在 Rebol 是开源的,我们终于可以了! :-)
    【解决方案2】:

    虽然问题最初问为什么return/redo 不评估块,但也有类似的表述:“很酷,因为你可以做诸如尾调用优化之类的事情”,“[可以编写]返回功能的包装器”, “我们越想它,它似乎就越有用”。

    我不认为这些是真的。我的第一个示例演示了一个真正使用return/redo 的案例,可以说是return/redo 的“专业领域”中的一个示例。它是一个可变参数求和函数,称为sumn

    use [result collect process] [
        collect: func [:value [any-type!]] [
            unless value? 'value [return process result]
            append/only result :value
            return/redo :collect
        ]
        process: func [block [block!] /local result] [
            result: 0
            foreach value reduce block [result: result + value]
            result
        ]
        sumn: func [] [
            result: copy []
            return/redo :collect
        ]
    ]
    

    这是用法示例:

    >> sumn 1 * 2 2 * 3 4
    == 12
    

    采用“无限数量”参数的可变参数函数在 Rebol 中并不像乍一看那样有用。例如,如果我们想在一个小脚本中使用 sumn 函数,我们必须将它包装到一个括号中以指示它应该停止收集参数的位置:

    result: (sumn 1 * 2 2 * 3 4)
    print result
    

    这并不比使用更标准(非可变)的替代方案更好,例如block-sum 只接受一个参数,一个块。用法是这样的

    result: block-sum [1 * 2 2 * 3 4]
    print result
    

    当然,如果函数能够以某种方式检测到它的最后一个参数是什么,而无需使用括号,那么我们真的会有所收获。在这种情况下,我们可以使用 #[unset!] 值作为 sumn 停止参数,但这也不能避免输入:

    result: sumn 1 * 2 2 * 3 4 #[unset!]
    print result
    

    看到return 包装器的示例,我会说return/redo 不太适合return 包装器,return 包装器超出了其专业领域。为了证明这一点,这里有一个用 Rebol 2 编写的 return 包装器,它实际上超出了 return/redo 的专业领域:

    myreturn: func [
        {my RETURN wrapper returning the string "indefinite" instead of #[unset!]}
        ; the [throw] attribute makes this function a RETURN wrapper in R2:
        [throw]
        value [any-type!] {the value to return}
    ] [
        either value? 'value [return :value] [return "indefinite"]
    ]
    

    在 R2 中测试:

    >> do does [return #[unset!]]
    >> do does [myreturn #[unset!]]
    == "indefinite"
    >> do does [return 1]
    == 1
    >> do does [myreturn 1]
    == 1
    >> do does [return 2 3]
    == 2
    >> do does [myreturn 2 3]
    == 2
    

    另外,我不认为return/redo 有助于尾调用优化。在www.rebol.org 站点上有一些示例如何在不使用return/redo 的情况下实现尾调用。如前所述,return/redo 是为支持可变参数函数的实现而量身定制的,就参数传递而言,它对于其他目的不够灵活。

    【讨论】:

    • sumn与R3的#none的简写语法结合起来至少可以节省一些输入:sumn 10 20 30 #
    • 我认为 ` \ ` 作为字面未设置的“表达式障碍”概念甚至更好。 x: sumn 10 20 30 \ print x...它提供了一个很好的工具来帮助作者显示边界,即使函数是非可变的(如果他们觉得这样做是说明性的)。 # 这个不太好用,IMO。
    猜你喜欢
    • 1970-01-01
    • 2011-07-17
    • 1970-01-01
    • 1970-01-01
    • 2021-12-16
    • 2015-07-20
    • 1970-01-01
    • 2020-06-06
    • 2013-01-27
    相关资源
    最近更新 更多