Как связаны слова в модуле Rebol?

Я понимаю, что тип module! обеспечивает лучшую структуру для защищенных пространств имен, чем object! или 'use. Как связаны слова в модуле - я замечаю некоторые ошибки, связанные с несвязанными словами:

REBOL [Type: 'module] set 'foo "Bar"

Кроме того, как Rebol проводит различие между локальным словом модуля ('foo) и системной функцией ('set)?

Незначительное обновление, вскоре после:

Я вижу там переключатель, который меняет метод привязки:

REBOL [Type: 'module Options: [isolate]] set 'foo "Bar"

Что это делает по-другому? Какие ошибки возникают при использовании этого метода по умолчанию?

Ответ 1

ОК, это будет немного сложно.

В Rebol 3 нет таких вещей, как системные слова, есть только слова. Некоторые слова добавлены в библиотеку времени выполнения lib, а set - одно из этих слов, которое, как оказалось, имеет назначенную ему функцию. Модули импортируют слова из lib, хотя то, что означает "импорт", зависит от параметров модуля. Это может быть более сложным, чем вы ожидали, поэтому позвольте мне объяснить.

Регулярные модули

Во-первых, я расскажу о том, какие средства импорта для "обычных" модулей, те, которые не имеют каких-либо опций. Начните с первого модуля:

REBOL [Type: 'module] set 'foo "Bar"

Прежде всего, у вас есть неправильное предположение: слово foo не является локальным для модуля, это точно так же, как set. Если вы хотите определить foo как локальное слово, вы должны использовать тот же метод, что и для объектов, используйте слово как заданное слово на верхнем уровне, например:

REBOL [Type: 'module] foo: "Bar"

Единственное различие между foo и set заключается в том, что вы еще не экспортировали или добавили слово foo в lib. Когда вы ссылаетесь на слова в модуле, который вы не объявили как локальные слова, он должен откуда-то получить свои значения и/или привязки. Для обычных модулей он сначала привязывает код к lib, затем переопределяет это, снова привязывая код к локальному контексту модуля. Любые слова, определенные в локальном контексте, будут привязаны к нему. Любые слова, не определенные в локальном контексте, сохраняют свои старые привязки, в данном случае - lib. Это то, что "импорт" означает для регулярных модулей.

В первом примере, предполагая, что вы этого не сделали, слово foo не было добавлено в библиотеку времени выполнения раньше времени. Это означает, что foo не привязан к lib, и поскольку он не был объявлен как локальное слово, он также не привязан к локальному контексту. Таким образом, foo не был связан ни с чем. В коде, который был ошибкой, но в другом коде это может быть не так.

Изолированные модули

Существует опция "isolate", которая изменяет способ импорта файлов, что делает его "изолированным" модулем. Позвольте использовать ваш второй пример здесь:

REBOL [Type: 'module Options: [isolate]] set 'foo "Bar"

Когда выполняется изолированный модуль, каждое слово в модуле, даже во вложенном коде, собирается в локальный контекст модуля. В этом случае это означает, что set и foo являются локальными словами. Начальные значения этих слов устанавливаются в значения, которые они имеют в lib, во время создания модуля. То есть, если слова определены в lib вообще. Если слова не имеют значений в lib, изначально они не будут иметь значения в модуле.

Важно отметить, что этот импорт значений является разовым. После этого первоначального импорта любые изменения этих слов, сделанные вне модуля, не влияют на слова в модуле. Вот почему мы говорим, что модуль "изолирован". В случае вашего примера кода это означает, что кто-то может изменить lib/set, и это не повлияет на ваш код.

Но есть еще один важный тип модуля, который вы пропустили...

Сценарии

В Rebol 3 скрипты - это еще один вид модуля. Здесь ваш код как script:

REBOL [] set 'foo "Bar"

Или, если хотите, поскольку заголовки script являются необязательными в Rebol 3:

set 'foo "Bar"

Сценарии также импортируют свои слова из lib, и они импортируют их в изолированный контекст, но с завихрением: все сценарии имеют один и тот же изолированный контекст, известный как "пользовательский" контекст. Это означает, что когда вы измените значение слова в script, следующий script, чтобы использовать это слово, увидит изменение при его запуске. Поэтому, если после запуска выше script, вы пытаетесь запустить этот:

print foo

Затем он напечатает "Bar", а не foo be undefined, хотя foo еще не определен в lib. Вам может показаться интересным знать, что если вы используете Rebol 3 в интерактивном режиме, вводя команды в консоль и получая результаты, каждая введенная вами командная строка представляет собой отдельный script. Поэтому, если ваш сеанс выглядит следующим образом:

>> x: 1
== 1
>> print x
1

Строки x: 1 и print x являются отдельными сценариями, а вторая использует изменения, внесенные в контекст пользователя, с помощью первого.

Пользовательский контекст на самом деле должен быть локальным, но на данный момент игнорировать это.

Почему разница?

Здесь мы возвращаемся к "системной функции", и у Ребола их нет. Функция set аналогична любой другой функции. Он может быть реализован по-разному, но он по-прежнему является нормальным значением, назначенным нормальному слову. Приложение должно будет управлять многими этими словами, поэтому у нас есть модули и библиотека времени выполнения.

В приложении будут вещи, которые необходимо изменить, и другие вещи, которые не должны меняться, и какие вещи зависят от приложения. Вы захотите объединить свои вещи, чтобы все было организовано или для контроля доступа. Там будут глобально определенные вещи и локально определенные вещи, и вы захотите иметь организованный способ получить глобальный материал в локальных местах и ​​наоборот, а также разрешить любые конфликты, когда более чем одна вещь хочет определить материал с помощью одно и то же имя.

В Rebol 3 мы используем модули для группировки, для удобства и контроля доступа. Мы используем библиотеку времени выполнения lib как место для сбора экспорта модулей и разрешения конфликтов, чтобы управлять тем, что импортируется в локальные места, такие как другие модули и пользовательский контекст (ы). Если вам нужно переопределить некоторые вещи, вы делаете это, изменяя библиотеку времени выполнения и, при необходимости, распространяя свои изменения в контексте (-ях) пользователя. Вы даже можете обновлять модули во время выполнения, а новая версия модуля переопределяет слова, экспортированные старой версией.

Для обычных модулей, когда вещи переопределены или обновлены, ваш модуль получит выгоду от таких изменений. Предполагая, что эти изменения являются преимуществом, это может быть хорошо. Регулярный модуль взаимодействует с другими регулярными модулями и сценариями, чтобы обеспечить совместную работу среды.

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

Недостатком изолирования является то, что, если есть изменения в библиотеке времени выполнения, которые могут вам понадобиться, вы не получите их. Если ваш модуль каким-то образом доступен (например, импортировался с именем), кто-то может распространять эти изменения для вас, но если вы недоступны, вам не повезло. Надеюсь, вы подумали отслеживать lib для желаемых изменений или напрямую ссылаться на материал через lib.

Тем не менее, мы пропустили еще одну важную проблему...

Экспорт

Другая часть управления библиотекой времени выполнения и всеми этими локальными контекстами экспортируется. Вы должны каким-то образом получить свои вещи. И самый важный фактор - это то, о чем вы не подозревали: имеет ли ваш модуль имя.

Имена являются необязательными для модулей Rebol 3. Сначала это может показаться просто способом упрощения написания модулей (и в оригинальном предложении Carl именно поэтому). Однако выясняется, что есть много вещей, которые вы можете сделать, когда у вас есть имя, которое вы не можете, когда вы этого не сделаете, просто из-за того, что такое имя: способ ссылаться на что-то. Если у вас нет имени, у вас нет способа ссылаться на что-то.

Это может показаться тривиальной вещью, но вот некоторые вещи, которые позволяет вам сделать:

  • Вы можете узнать, загружен ли модуль.
  • Вы можете убедиться, что модуль загружен только один раз.
  • Вы можете узнать, была ли более ранняя версия модуля раньше и возможно обновить ее.
  • Вы можете получить доступ к ранее загруженному модулю.

Когда Карл решил сделать имена факультативными, он дал нам такую ​​ситуацию, когда можно было бы создавать модули, для которых вы не могли бы сделать ничего из этого. Учитывая, что экспорт модулей должен собираться и организовываться в библиотеке времени выполнения, у нас была ситуация, когда вы могли иметь влияние на библиотеку, которую вы не могли легко обнаружить, и модули, которые загружались каждый раз, когда они были импортированы.

Итак, для безопасности мы решили полностью вырезать библиотеку времени выполнения и просто экспортировать слова из этих неназванных модулей непосредственно в локальные (модули или пользовательские) контексты, которые импортировали их. Это делает эти модули эффективными частными, как будто они принадлежат целевым контекстам. Мы столкнулись с потенциально неудобной ситуацией и сделали ее особенной.

Это была такая особенность, что мы решили поддержать ее явно с помощью опции private. Это явный вариант помогает нам справиться с последней проблемой, не вызвавшей нас именем: заставить частные модули не перезагружать снова и снова. Если вы даете модулю имя, его экспорт все равно может быть закрытым, но ему нужна только одна копия того, что он экспортирует.

Однако имя или нет, личное или нет, то есть 3 типа экспорта.

Регулярные именованные модули

Возьмем этот модуль:

REBOL [type: module name: foo] export bar: 1

Импортирование добавляет модуль в список загруженных модулей с версией по умолчанию 0.0.0 и экспортирует одно слово bar в библиотеку времени выполнения. "Экспорт" в этом случае означает добавление слова bar в библиотеку времени выполнения, если оно там отсутствует, и установка этого слова lib/bar в значение, которое имеет слово foo/bar после завершения foo (если он уже не установлен).

Стоит отметить, что этот автоматический экспорт происходит только один раз, когда тело foo завершено. Если после этого вы измените на foo/bar, это не повлияет на lib/bar. Если вы хотите изменить lib/bar тоже, вы должны сделать это вручную.

Также стоит отметить, что если lib/bar уже существует до импорта foo, у вас не будет добавлено другое слово. И если lib/bar уже установлено значение (не отключено), импорт foo не будет перезаписывать существующее значение. Первым прибыл - первым обслужен Эквивалент в русском языке: поздний гость гложет и кость. Если вы хотите переопределить существующее значение lib/bar, вам придется сделать это вручную. Так мы используем lib для управления переопределениями.

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

Именованные частные модули

В некоторых случаях вы не хотите экспортировать свои материалы в основную библиотеку времени выполнения. Материал в lib импортируется во все, поэтому вы должны экспортировать материал только в lib, который вы хотите сделать общедоступным. Иногда вы хотите создавать модули, которые только экспортируют материал для контекстов, которые этого хотят. Иногда у вас есть некоторые связанные модули, общая установка и служебный модуль или около того. Если это так, вы можете создать частный модуль.

Возьмем этот модуль:

REBOL [type: module name: foo options: [private]] export bar: 1

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

Этот вид полезен для создания модулей утилиты, расширенных API-интерфейсов или других подобных трюков. Это также полезно для создания сильного модульного кода, который требует явного импорта, если это то, чем вы занимаетесь.

Стоит отметить, что если ваш модуль фактически ничего не экспортирует, нет никакой разницы между именованным частным модулем или именованным открытым модулем, поэтому он в основном рассматривается как открытый. Все, что имеет значение, это то, что у него есть имя. Это подводит нас к...

Безмозглые модули

Как объяснялось выше, если ваш модуль не имеет имени, он в значительной степени должен рассматриваться как закрытый. Однако более частный, поскольку вы не можете сказать, загружен ли он, вы не можете его обновить или даже не перезагружать. Но что, если это то, что вы хотите?

В некоторых случаях вы действительно хотите, чтобы ваш код работал для эффекта. В этих случаях каждый раз, когда ваш код повторяется, вы хотите сделать. Возможно, это script, который вы используете с do, но структурируете как модуль, чтобы избежать утечки слов. Возможно, вы создаете mixin, некоторые функции утилиты, которые имеют некоторое локальное состояние, которое требует инициализации. Это может быть что угодно.

Я часто делаю свой файл %rebol.r неназванным модулем, потому что я хочу иметь больше контроля над тем, что он экспортирует и как. Кроме того, поскольку это сделано для эффекта и не нужно перезагружать или обновлять, нет смысла указывать ему имя.

Нет необходимости в примере кода, ваши предыдущие будут действовать таким образом.

Надеюсь, это даст вам достаточно обзора дизайна модульной системы R3.