Почему return/redo оценивает функции результата в вызывающем контексте, но результаты блока не оцениваются?

Прошлой ночью я узнал об опции /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

person HostileFork says dont trust SE    schedule 07.02.2013    source источник


Ответы (2)


Короткий ответ заключается в том, что, как правило, нет необходимости оценивать блок в точке вызова, поскольку блоки в 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 и кодом в функции:

>> 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 функции действительно необычны.

В большинстве языков программирования, которые могут вызывать значение функции, вы должны передавать параметры функции как полный набор, что-то вроде того, как работает функция apply в R3. Обычный вызов функции Rebol приводит к тому, что для ее аргументов происходит некоторое неизвестное заранее количество дополнительных вычислений с использованием неизвестных заранее правил вычисления. Оценщик вычисляет эти правила оценки во время выполнения и просто передает результаты оценки в функцию. Сама функция не обрабатывает оценку своих параметров и даже не обязательно знает, как оценивались эти параметры.

Однако когда вы do указываете значение функции явно, это означает передачу значения функции вызову другой функции, обычной функции с именем do, а затем это волшебным образом вызывает вычисление дополнительных параметров, которые вообще не передавались функции do.

Ну это не магия, это return/redo. Способ do работы функции заключается в том, что она возвращает ссылку на функцию в обычном возвращаемом значении быстрого доступа с флагом в значении возврата быстрого доступа, который сообщает интерпретатору, что вызвал do для оценки возвращает функцию, как если бы она была вызвана прямо в коде. Это в основном то, что называется батутом.

Здесь мы подходим к еще одной интересной особенности Rebol: возможность быстрого возврата значений из функции встроена в оценщик, но на самом деле он не использует для этого функцию return. Все функции, которые вы видите в коде Rebol, являются оболочками для внутренних вещей, даже return и do. Функция return, которую мы вызываем, просто генерирует одно из этих значений возврата быстрого доступа и возвращает его; все остальное делает оценщик.

Итак, в данном случае на самом деле произошло то, что все это время у нас был код, который делал то, что return/redo делает внутри, но Карл решил добавить опцию в нашу функцию return, чтобы установить этот флаг, хотя внутреннему коду не требуется return для выполнения так как внутренний код вызывает внутреннюю функцию. И потом он никому не сказал, что делает эту опцию доступной извне, или почему, или что она делает (думаю, вы не можете упомянуть все; у кого есть время?). У меня есть подозрение, основанное на разговорах с Карлом и некоторых ошибках, которые мы исправляли, что R2 обработал do функции по-другому, таким образом, что return/redo стало бы невозможным.

Это означает, что обработка return/redo довольно тщательно ориентирована на вычисление функций, поскольку это единственная причина его существования. Добавление к нему каких-либо накладных расходов приведет к увеличению накладных расходов на do функции, и мы используем их часто. Вероятно, не стоит распространять его на блоки, учитывая, как мало мы получим и как редко вообще получим какую-либо выгоду.

Что касается return/redo функции, кажется, что чем больше мы о ней думаем, тем она становится все более и более полезной. За последний день мы придумали всевозможные уловки, которые позволяют это сделать. Батуты полезны.

person BrianH    schedule 08.02.2013
comment
Спасибо за реверс-инжиниринг, просматривающий код, чтобы ответить на этот вопрос. Я часто отвечаю здесь на вопросы о вещах с открытым исходным кодом, говоря ну, это открытый исходный код, посмотрите. Теперь, когда исходный код Rebol открыт, мы наконец-то можем! :-) - person HostileFork says dont trust SE; 08.02.2013

Хотя первоначально вопрос задавался, почему 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 не входят в его область знаний. Чтобы продемонстрировать это, вот оболочка 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 помогает с оптимизацией хвостовых вызовов. На сайте www.rebol.org есть примеры реализации хвостовых вызовов без использования return/redo. Как было сказано, return/redo был создан специально для поддержки реализации функций с переменным числом аргументов, и он недостаточно гибок для других целей в том, что касается передачи аргументов.

person Ladislav    schedule 15.02.2013
comment
Комбинация sumn с сокращенным синтаксисом # R3 для none могла бы, по крайней мере, немного сэкономить на наборе текста: sumn 10 20 30 #. - person earl; 16.02.2013
comment
Я думаю, что понятие барьера выражений `\` как буквальное неустановленное еще лучше. x: sumn 10 20 30 \ print x... и это хороший инструмент, помогающий авторам показать границу, даже если функция не является вариативной (если они сочтут это иллюстративным). # для этого не подходит, IMO. - person HostileFork says dont trust SE; 14.07.2015