Почему функция 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
2 ответа
Короткий ответ заключается в том, что, как правило, нет необходимости оценивать блок в точке вызова, поскольку блоки в Rebol не принимают параметры, поэтому в основном не имеет значения, где они оцениваются. Тем не менее, это "в основном" может потребовать некоторых объяснений...
Это сводится к двум интересным особенностям Rebol: статическое связывание и как do
функции работает.
Статическое связывание и области
У Rebol нет привязок слов с ограничениями, у него есть статические прямые привязки слов. Иногда кажется, что у нас есть лексическая область действия, но мы действительно притворяемся, обновляя статические привязки каждый раз, когда мы строим новый блок кода "scoped". Мы также можем перепривязывать слова вручную, когда захотим.
Для нас в этом случае это означает, что, как только блок существует, его привязки и значения являются статическими - на них не влияет то, где блок находится физически или где он оценивается.
Тем не менее, и здесь все становится сложнее, контексты функций странные. Хотя привязки слов, привязанных к контексту функции, являются статическими, набор значений, назначенных этим словам, динамически ограничивается. Это побочный эффект от того, как код оценивается в 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
совсем?
Как работает функция
В основном мы имеем return/redo
потому что он использует точно такой же код, который мы используем для реализации do
функции. Вы можете не осознавать этого, но do
функции действительно необычны.
В большинстве языков программирования, которые могут вызывать значение функции, вы должны передавать параметры в функцию как полный набор, как R3 apply
функция работает. Регулярный вызов функции 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
функции, тем не менее, она становится все более и более полезной, чем больше мы думаем об этом. В последний день мы придумали всевозможные уловки, которые это позволяет. Батуты полезны.
Хотя вопрос изначально задавался почему 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
Функции Variadic, принимающие "неограниченное количество" аргументов, не так полезны в Rebol, как это может показаться на первый взгляд. Например, если мы хотим использовать sumn
функция в небольшом скрипте, мы должны были бы обернуть его в парен, чтобы указать, где он должен прекратить собирать аргументы
result: (sumn 1 * 2 2 * 3 4)
print result
Это не лучше, чем использование более стандартной (не вариационной) альтернативы, называемой, например, block-sum
и принимая только один аргумент, блок. Использование будет как
result: block-sum [1 * 2 2 * 3 4]
print result
Конечно, если функция может каким-то образом определить, что является ее последним аргументом, без необходимости включать paren, мы действительно получим что-то. В этом случае мы могли бы использовать #[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/redo
на сайте http://www.rebol.org/. Как сказано, return/redo
был специально создан для поддержки реализации функций с переменным числом аргументов, и он недостаточно гибок для других целей в отношении передачи аргументов.