Есть ли общее объяснение области определения в Rebol и Red
Из REBOL/Основное руководство пользователя и Что такое красный, я узнал, что оба Rebol и Red используют определение области.
Из руководства, я знаю, что это форма статического охвата, "область действия переменной определяется при определении ее контекста" и также называется лексическая область выполнения и динамическая форма статического охвата, которая зависит от контекста
определения.
Я знаю, что в com-sci есть две формы обзора: лексическая область (статическая область) и динамическое масштабирование. Это определение показало меня.
Итак, что такое определение?
Ответы
Ответ 1
Ребол фактически не имеет области видимости.
Возьмем этот код:
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
использует a
из внешней области, a
, используемый func-2
, из внутренней области, а 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 не делает ничего из этого; в частности, он не компилируется, он встроен во время выполнения. То, что вы считаете кодом, это на самом деле данные, блоки слов, цифры и т.д. Слова представляют собой структуры данных, в которых указатель в них называется "привязкой".
При первом загрузке script все слова в script добавляются в объект среды script (который мы неправильно вызываем "контекст", хотя это не так). Пока слова собираются, данные script изменяются. Любое слово, найденное в контексте script ", связано с" контекстом "или" связан ". Эти привязки означают, что вы можете просто следовать этой одной ссылке и перейти к объекту, где хранится значение этого слова. Это очень быстро.
Затем, как только это будет сделано, мы запустим script. И тогда мы добираемся до этого бита: func [] [a]
. На самом деле это не объявление, это вызов функции с именем func
, которая принимает блок spec и блок кода и использует их для построения функции. Эта функция также получает свой собственный объект среды, но со словами, объявленными в spec функции. В этом случае в спецификации нет слов, поэтому это пустой объект. Затем блок кода привязан к этому объекту. Но в этом случае в этом объекте нет a
, поэтому ничего не делается с a
, он сохраняет привязку, которая уже имела, когда она была связана раньше.
То же самое относится к вызову context [...]
- да, что вызов функции неадекватно назван context
, который строит объект, вызывая make object!
. Функция context
принимает блок данных и ищет заданные слова (эти вещи с хвостовыми двоеточиями, например a:
), затем строит объект с этими словами в нем, а затем связывает все слова в этот блок и все вложенные блоки соответствуют словам, находящимся в объекте, в данном случае a
, func-2
и func-3
. И это означает, что a
в этом блоке кода изменили свои привязки, вместо этого указывая на этот объект.
Когда func-2
определено, привязка a
в его кодовом блоке не переопределяется. Когда func-3
определен, он имеет a
в своей спецификации, поэтому a:
имеет привязку привязки.
Самое смешное в том, что вообще нет никаких областей. Этот первый a:
и a
в теге func-1
только один раз привязан, поэтому они сохраняют свою первую привязку. Блок кода a:
в inner
и a
в func-2
связаны дважды, поэтому они сохраняют свою вторую привязку. Код a:
in func-3
связан три раза, поэтому он также сохраняет свою последнюю привязку. Он не ограничен, он просто привязан к коду, а затем более мелкие биты кода снова связаны и так далее, пока это не будет сделано.
Каждый раунд привязки выполняется функцией, которая "определяет" что-то (на самом деле, строит ее), а затем, когда этот код работает и вызывает другие функции, которые определяют что-то еще, эти функции выполняют другой раунд привязки к его маленькому подмножество кода. Вот почему мы называем это "областью определения"; в то время как это действительно не охват, это то, что служит цели обзора в Rebol, и оно достаточно близко к поведению лексического охвата, которое, на первый взгляд, вы не можете отличить.
Это действительно становится другим, когда вы понимаете, что эти привязки являются прямыми, и вы можете их изменить (например, вы можете создавать новые слова с тем же именем и другой привязкой). Та же самая функция, что и функции определения, вы можете назвать себя: она называется bind
. С помощью bind
вы можете сломать иллюзию области видимости и сделать слова, которые привязаны к любому объекту, к которому вы можете получить доступ. Вы можете делать замечательные трюки с помощью bind
, даже создавать свои собственные функции определения. Это весело!
Что касается Red, Red компилируется, но также включает в себя интерпретатор Rebol, привязку и все полезные свойства. Когда он определяет вещи с интерпретатором, он также определяет область определения.
Помогает ли это сделать более понятным?
Ответ 2
Это старый вопрос, и ответ @BrianH здесь очень подробен в механике. Но я подумал, что я дам чуть-чуть другой фокус, как немного больше "истории".
В Rebol существует категория типов words. Это, по сути, символы, поэтому их строковое содержимое сканируется, и они входят в таблицу символов. Так что если "FOO"
будет строкой, а <FOO>
будет еще одним "ароматом" строки, известной как тег... FOO
, 'FOO
, FOO:
и :FOO
- всевозможные "ароматы", слов с тем же символом. ( "Слово", "освещенное слово", "заданное слово" и "get-word" соответственно.)
Сбрасывание в символ делает невозможным изменение имени слова после загрузки. Они застревают, по сравнению со строками, каждая из которых имеет свои собственные данные и изменена:
>> append "foo" "bar"
== "foobar"
>> append 'foo 'bar
** Script error: append does not allow word! for its series argument
Иммутируемость имеет преимущество в том, что в качестве символа быстро сравнивать одно слово с другим. Но есть еще одна часть головоломки: каждый экземпляр слова может необязательно иметь в себе невидимое свойство, называемое привязкой. Это связывание позволяет "указывать на" объект ключа/значения, известный как контекст, где значение может быть прочитано или записано.
Примечание. В отличие от @BrianH, я не думаю, что вызов этой категории целей привязки "контекстов" - это все так плохо - по крайней мере, я не думаю, что это сегодня. Спросите меня позже, я могу передумать, если появятся новые доказательства. Достаточно сказать, что это объектная вещь, но не всегда объект... это может быть ссылка на фрейм функции в стеке, например.
Тот, кто приносит слово в систему, получает первый выстрел, чтобы сказать, в каком контексте он связан. Много времени, что LOAD, поэтому, если вы сказали 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 получил два блока, один из которых представляет список параметров, а второй - тело. Когда он получил тело, оба print
были привязаны к встроенной функции печати (в данном случае - и важно отметить, что когда вы получаете материал из других мест, кроме консоли, каждый из них может быть связан по-разному!). x
был привязан к контексту пользователя (в данном случае), который держал значение 10. Если FUNC ничего не сделал для изменения ситуации, все оставалось бы таким же образом.
Но он поставил изображение вместе и решил, что, поскольку в списке параметров есть x в нем, он будет просматривать тело и перезаписывать слова с идентификатором символа для x с новым связыванием... локальным для функции, Это единственная причина, по которой он не перезаписывал глобальное значение с помощью x: 20
. Если бы вы опустили [x] в spec FUNC, ничего бы не сделали, и он был бы перезаписан.
Каждая часть в цепочке определения получает возможность, прежде чем передавать вещи. Следовательно, определение области определения.
FUN FACT: Поскольку, если вы не задаете параметры спецификации FUNC, она не будет перепроверять что-либо в теле, это привело к ошибочным впечатлениям о том, что "все в Rebol находится в глобальном масштабе". Но это не так, потому что, как @BrianH говорит: "Ребол на самом деле вообще не имеет границ (...) Rebol подделывает его". Фактически, это то, что делает FUNCTION (в отличие от FUNC) - он отправляется на охоту в теле для заданных слов, таких как x:, а когда видит их, добавляет их в локальный фрейм и связывается с ним их. Эффект выглядит как локальный масштаб, но опять же, это не так!
Если звучит немного Rube-Goldberg-esque, чтобы представить эти символы с невидимыми указателями, которые перетасовываются вокруг, это потому, что это так. Для меня лично замечательная вещь - это то, что она работает вообще... и я видел, как люди тянут с ней трюки, которые вы не могли бы интуитивно думать, что такой простой трюк можно было бы использовать.
Пример: безумно полезный COLLECT и KEEP (версия Ren-C):
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-each
и length?
для length
)
>> collect [
keep 10
for-each item [a [b c] [d e f]] [
either all [
block? item
3 = length item
][
keep/only item
][
keep item
]
]
]
== [10 a b c [d e f]]
Трюк здесь с областью определения определения лучше всего понимается по тому, что я упомянул выше. FUNC только перезаписывает привязки вещей в списке параметров и оставляет все остальное в теле нетронутым. Итак, что происходит, так это то, что он принимает тело, которое вы передали COLLECT, и использует его как тело новой функции, где он перезаписывает любые привязки KEEP. Затем он устанавливает KEEP в функцию, которая добавляет данные в агрегатор при вызове.
Здесь мы видим универсальность функции KEEP в блоках сплайсинга в собранном выходе или нет, с помощью переключателя /ONLY (вызывающий выбирает не сращивать, только если мы видим элемент длиной 3). Но это только царапает поверхность. Это всего лишь одна очень мощная языковая функция, добавленная пользователями после факта - из-за небольшого кода это почти страшно. Есть, конечно, много других историй.
Я здесь добавляю ответ из-за того, что заполнил ключевую недостающую ссылку для определения области определения, проблему, известную как "определение с ограниченным охватом":
https://codereview.stackexchange.com/info/109443/definitional-returns-solved-mostly
Вот почему <with> return
находится рядом с KEEP в спецификации. Это происходит потому, что COLLECT пытается рассказать FUNC, что он хочет "использовать свои службы" как связующее звено и код. Но тело уже было написано кем-то другим. Поэтому, если в нем есть ВОЗВРАТ, тогда RETURN уже имеет представление о том, где вернуться. FUNC предназначен только для "повторного охвата" хранилища, но оставляйте только одни возвращения, а не добавляйте их собственные. Следовательно:
>> foo: func [x] [
collect [
if x = 10 [return "didn't collect"]
keep x
keep 20
]
]
>> foo 304
== [304 20]
>> foo 10
== "didn't collect"
Это with <return>
, что делает COLLECT способным быть достаточно умным, чтобы знать, что внутри тела FOO он не хотел возвратного отскока, так что он думал вернуться к функции, параметр которой был просто [сохранить].
И немного о "почему" определения области определения, а не просто "что".: -)