REBOL: Контекст против разума
Рабочие материалы по почти функциональному и почти объектному языку программирования

Контекст против разума

© pat665@rebol-list — две оригинальные главы в тексте
© «Бакава» Анохин — идея и воплощение

© Некоторые представители ноосферы — вдохновение и идеи
© «Молчувка» — редактура

Содержание: 1. Контексты, тексты, тесты
2. Чем context отличается от контекста?
3. Простые примеры
4. Имена и значения функций
4.1 Зачем нужен 'bind
4.2 Как изменить привязку
5. Примеры применения bind
5.1 Диалектные функции
5.2 Доступ внутрь цикла
5.3 Частичное связывание

1. Контексты, тексты, тесты

Контекст -- это условия употребления слова, позволяющие уточнить значение этого слова. В литературе так называют и некоторую окрестность текста вокруг данного слова (как, например, в выражении "исходить из контекста"). Большинство языков программирования игнорируют контексты, но в человеческих языках от контекста зачастую зависит трактовка всего текста: сравним "...шкура у него толстая..." с "...ну он и шкура!..".

Rebol является контекстно-зависимым языком: значение слова (а следовательно, и результат его выполнения) зависит от контекста выполнения. Вот пример контекстно-зависимого кода:

textcon: [ print 333 ]
== [print 333]

Дальше мы можем использовать это слово в контексте выполняемого кода:

do textcon
== 333

или, например, в контексте последовательностей:

textcon/1
== print

2. Чем context отличается от контекста?

Словарь языка Rebol содержит слово context. Как же оно связано с понятием контекста, которое мы только что усвоили?

Никак. Почти никак. Не верите? Смотрите сами:

? context

USAGE:
    CONTEXT blk

DESCRIPTION:
    Defines a unique (underived) object.
    CONTEXT is a function value.

ARGUMENTS:
    blk -- Object variables and values. (Type: block)

Итак, слово context порождает прототип -- объект-сироту, у которого нет родителей, но могут быть дети. Например,

a: context [a: 4]
b: make! a [b: 5]
a
a/a
== 4
b
b/a
== 4
b/b
== 5

Что произошло? Мы создали прототип a, в котором переменная а имеет значение 4. Затем мы создали наследника а, объект b, содержащий и переменную а, и новую переменную b. Выполнение прототипа и его наследника не возвращает никаких значений, зато переменные отдают свои значения. Подробнее об объектах в rebol-стиле лучше почитать в статье Объекты и функции. Можно формировать и более изощрённые структуры:

a: context [ a: context [ a: context [ a: 4 ] ] ]
a
a/a
a/a/a
a/a/a/a
== 4
? a
A is an object of value:
a               object!   [a]

Я, к сожалению, так и не придумал, где такие странности можно использовать.

3. Простые примеры

Посмотрим, как ведёт себя переменная в определённом контексте и без него, например:

for x 1 8 2 [print x]
==1
==3
==5
==7
print x
** Script Error: x has no value
** Near: print x

Итак, х существует только внутри цикла (или, как выражаются теоретики, x определён лишь во временном пространстве имён). Вне цикла доступ к x невозможен: мы скорее сможем опрееделить новый x в глобальном контексте:

x: 100
== 100
for x 1 8 2 [print x]
==1
==3
==5
==7
print x
==100

Итак, мы убедились в существовании нескольких контекстов, в одном из которых x имеет значение 7 (последнее значение, полученное переменной в цикле), а в другом -- глобальном -- значение 100.

Можно ли достучаться до глобальной переменной из контекста цикла? Да, можно:

for x 1 3 1 [print rebol/words/x]
==100
==100
==100

Объект rebol/words содержит все слова:

first rebol/words
== [end! unset! error! datatype! context! native! action! routine!...

Исследуем какой-нибудь объект:

>> o: make object! [ a: 1 b: 2 d: does [print [a b c] ] ]
>> first o
== [self a b d]
>> second o
== [make object! [
        a: 1
        b: 2
        d: func [][print [a b c]]
    ] 1 2 func [][print [a b c]]]
>> 

Итак, объекты состоят из двух блоков: блока имён (в o это [self a b d]) и блока значений (второй блок с хитрой структурой).

4. Имена и значения функций

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

o: make object! [
    print: func [b [block!]][rebol/words/print compose ["o-print-> : " (b)]]
    test:  does [print ["test"]]
]
>> o/test
o-print-> :  test

Что произошло? Мы определили локальную функцию print, и отныне все обращения к слову print внутри объекта o будут печатать блок данных, предваряемый невнятным префиксом. К сожалению, более осмысленные примеры мы готовим на завтрак.

4.1. Зачем нужен 'bind ?

Добавим-ка ещё одну функцию, выполняющую некий блок кода, данный ей в качестве аргумента:

o: make object! [
    print: func [b [block!]][rebol/words/print compose ["o-print-> : " (b)]]
    test1: does [ print ["test"]]
    test2: func [b [block!]][do b]
]

Теперь попробуем напечатать какую-нибудь белиберду!"

>> o/test2 [print ["Привет, Ктулху!"]]
Привет, Ктулху!
>>

Хм, а куда делась локальная версия print?

Внутри блока [print ["Привет, Ктулху!"]] слово 'print привязано к глобальному контексту, и будет к нему привязано, если мы его не перевяжем заново. Для этого и используется слово bind.

o: make object! [
    print: func [b [block!]][rebol/words/print compose ["o-print-> : " (b)]]
    test1: does [ print ["test"]]
    test2: func [b [block!]][do b]
    test3: func [b [block!]][do bind b 'self]
]

>> o/test3 [print ["hello World!"]]
o-print-> :  hello World!

Вот так мы привязали слово b (т.е. блок-аргумент) к контексту объекта o. Слово self существует в каждом объекте, и его контекст -- контекст внутри родительского объекта. На самом деле, можно использовать любое другое слово, характерное для этого объекта: 'test1,'test2, даже 'print.

Заметьте, что если bind выполняется для блока, эффект "привязывания" к контексту не исчезает после использования:

>> myBlock: [print ["Hello World!"]]
== [print ["Hello World!"]]
>> do myBlock
Hello World!
>> o/test3 myBlock
o-print-> :  Hello World!
>> do myBlock
o-print-> :  Hello World!

Эффект этот довольно мощен, но не всегда желателен, поэтому лучше связывать копии блоков:

o: make object! [
    print: func [b [block!]][rebol/words/print compose ["o-print-> : " (b)]]
    test1: does [ print ["test"]]
    test2: func [b [block!]][do b]
    test3: func [b [block!]][do bind copy b 'self]
]

Теперь всё вроде бы логично:

>> myBlock: [print ["Hello World!"]]
== [print ["Hello World!"]]
>> do myBlock
Hello World!
>> o/test3 myBlock
o-print-> :  Hello World!
>> do myBlock
Hello World!

Кроме того, можно воспользоваться bind/copy, результат будет тот же.

4.2. Как изменить привязку

Мы можем вернуть привязку блока к глобальному контексту. Для этого мы должны воспользоваться каким-нибудь глобальным словом. Рекомендуется использовать для этой цели слово system:

>> do myBlock
o-print-> :  Hello World!
>> bind myBlock 'system
== [print ["Hello World!"]]
>> do myBlock
Hello World!

5. Примеры применения bind

Для программирования в определённом стиле использование bind просто необходимо -- без связывания практически неосуществимо метапрограммирование. Рассмотрим основные (и потому довольно примитивные) варианты использования связывания.

5.1 Диалектные функции

Хм, а зачем вообще переименовывать функцию print? Можно было воспользоваться словом myprint и избежать проблем со связыванием слова с контекстом? Да, можно поступить и так.

o: make object! [
    myprint: func [b [block!]][print compose ["o-print-> : " (b)]]
    test1: does [ myprint ["test"]]
    test2: func [b [block!]][do b]
    test3: func [b [block!]][do bind copy b 'self]
]
>> o/test1
o-print-> :  test

Получилось!

>> myblock: [myprint ["Hello World!"]]
== [myprint ["Hello World!"]]

>> o/test2 myblock
** Script Error: myprint has no value
** Where: test2
** Near: myprint ["Hello World!"]

А здесь, увы, не получилось, поскольку myprint связан с глобальным контекстом, в котором у него нет значения.

>> o/test3 myblock
o-print-> :  Hello World!

Такие задачи встречаются чаще всего при создании диалекта. Тогда требуется построить блоки, содержащие диалектные функции, которые могут быть использованы вне диалекта. Хороший пример построения диалекта можно найти в статье TUI Dialect - A dialect to print ASCII sequences in REBOL".

5.2. Доступ внутрь цикла

В рассылке rebol-list был выловлен ещё один пример простого использования связывания. Вот начальная задача:

block: ["a" "b" "c"]
code: [print member]
foreach member block [do code]
** Script Error: member has no value
** Where: do-boot
** Near: print member

Здесь слово 'member внутри блока 'code связано с глобальным контекстом (и в нём у него нет значения!). Слово 'member в цикле foreach -- это новое слово, связанное с контекстом цикла. Нужно связать блок code c локальным словом 'member.

>>  block: ["a" "b" "c"]
== ["a" "b" "c"]
>>  code: [print member]
== [print member]
>>  foreach member block [do bind/copy code 'member]
a
b
c

Теперь слово 'member в блоке 'code связано с контекстом цикла, в котором и определено его значение.

5.3. Частичное связывание

Связывание не изменяет значение всех слов в блоке, но лишь тех из них, у которых есть локальные дубликаты.

glorp: "I'm global"
glunk: "I'm global too"
wordblock: [glorp glunk]

some-func: func [b [block!] /local glorp foo][
    glorp: "I'm local"
    foo: 42
    print [glorp]
    print b
    bind b 'foo
    print b
]

>> print wordblock
I'm global I'm global too

Всё глобально, верно?

>> some-func wordblock
I'm local
I'm global I'm global too
I'm local I'm global too

Но после связывания glorp в 'wordblock связан с контекстом some-func.

После связывания glunk в 'wordblock остался неизменным, поскольку в контексте foo нет одноимённых слов.

>> print wordblock
I'm local I'm global too
© «Бакава» Анохин
Formatted with MakeDoc2 by REBOL
4-Jun-2007