7

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

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

2 回答 2

7

简短的回答是因为通常没有必要在调用点评估块,因为 Rebol 中的块不带参数,所以在哪里评估它们几乎无关紧要。但是,“大部分”可能需要一些解释......

它归结为 Rebol 的两个有趣的特性:静态绑定和do函数的工作方式。

静态绑定和范围

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

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

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

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

无论你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在函数中使用 and 代码:

>> 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在函数中使用 and 代码:

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

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

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

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

函数的 DO 是如何工作的

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

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

然而,当你do显式地传递一个函数值时,这意味着将函数值传递给另一个函数的调用,一个名为 的常规函数do​​,然后神奇地导致对甚至根本没有传递给do函数的附加参数的求值。

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

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

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

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

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

于 2013-02-08T00:30:35.477 回答
4

虽然问题最初问为什么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

这并不比使用称为 eg 的更标准(非可变)替代方案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包装器超出了它的专业领域。为了证明这一点,这里有一个return用 Rebol 2 编写的包装器,它实际上超出了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对尾调用优化没有帮助。return/redowww.rebol.org网站上有示例如何在不使用的情况下实现尾调用。如前所述,return/redo它是为支持可变参数函数的实现而量身定制的,就参数传递而言,它对于其他目的不够灵活。

于 2013-02-15T07:53:03.563 回答