Древняя, новая, будущая (продолжение)

1 апрель, 2004 - 23:00Андрей Зубинский
На этот раз мы продолжим начатый ранее разговор о подстановках в Tcl. Фундаментальный и фундаментально простой механизм командной подстановки (МКП) мы уже рассмотрели. Давайте "испытаем" его на простом примере, использующем только то крохотное подмножество Tcl, о котором шла речь ранее. Попробуйте в консоли tkcons задать такую строку:

set A [set B 1]

и не спешите нажимать клавишу Enter. Давайте подумаем... Команда "set", как нам известно, при получении двух параметров создает переменную (первый параметр становится ее именем), присваивает ей значение (второй параметр) и возвращает результат выполнения операции -- присвоенное значение. То есть, "set A 1" -- это синтаксический аналог набора операций, описываемых псевдокодом

"создать A; A=”1”; возвратить значение A"

(закавыченная единица подчеркивает, что для Tcl "цифры" -- всего лишь символы). Но в набранном примере не все так просто: "машина Tcl" на этапе группировки, сканируя символы слева направо, "натыкается" в нем на символ "[", сигнализирующий о начале "новой ленты" (о "лентах" и аналогиях с машиной Тьюринга -- в предыдущей статье цикла). Далее "лента", содержащая текст "set B 1", передается рекурсивно вызванной машине Tcl, которая и выполняет набор операций

"создать B; B=”1”; возвратить значение B"

Возвращенным значением, очевидно, будет "1". И оно используется вызвавшей машиной Tcl благодаря МКП, замещающему подстроку [set B 1] на "1". Соответственно, вызвавшая машина создаст переменную A и присвоит ей это значение. Теперь мы можем записать более компактный (но, увы, не отражающий сути получения результата) псевдокод для всей операции set A [set B 1]:

"A=B=1"

(излишняя очевидная многословность удалена). В общем случае справедливо "равенство" между идиомой Tcl и псевдокодом

"set V1 [set V2 [set V3 [... [set VN Data]]... ]]" ≡ "V1=V2=V3...=VN=Data"

Эта действительно идиоматическая форма множественного присваивания в Tcl имеет свои особенности. Так, многократный рекурсивный вызов интерпретатора -- дело ресурсоемкое, поэтому подобная идиома хороша разве что тогда, когда глубина рекурсии невелика и собственно кодом требуется подчеркнуть, что группа переменных инициализируется одним и тем же значением. На самом деле в Tcl подобная задача решается разными методами и куда более эффективно (и даже изящнее), так что считайте этот пример сугубо учебно-демонстративным. Однако на его основе вы вправе построить ряд забавных идиом, например "set [set A B] Значение" и, в более общем виде (предполагая, что переменная A создана где-то ранее и содержит некоторое значение) -- "set [set A] Значение". Этот "почти трюк", если хотите, назовем "косвенным созданием переменной" (аналог косвенной адресации на уровне машинных команд) -- одной строкой мы создаем переменную с именем, являющимся значением переменной A, и присваиваем ей "Значение". Казалось бы, зачем это может понадобиться? Ответ прост -- налицо один из приемов, используемых для разработки Tcl-скриптов, генерирующих... Tcl-скрипты (хотя написание таких программ -- "высший пилотаж", ничего особо страшного в них нет).

Второй механизм подстановки Tcl, по сути, оказывается "синтаксическим сахаром" (syntax shugar -- так принято называть сокращающее количество нажатий на клавиши синтаксическое упрощение -- программисты, как известно, ленивы). Механизм "долларовой подстановки" (МДП) инициируется в исходных текстах фрагментами вида $Name и является сокращением записи [set Name]. Одно из достоинств МДП -- упрощение и повышение "читабельности" исходного кода, в котором используются, в частности, приемы косвенного создания переменных (или аналогичные им). Иначе говоря, МДП позволяет избежать излишества скобок, свойственного Lisp-подобным языкам. Что касается семантики, то она очевидна -- $Name в исходной программе на этапе подстановки заменяется значением переменной с именем Name. Соответственно приведенный ранее трюковый пример можно записать так: "set $A Значение" (создать переменную с именем, представляющим собой значение переменной A, и присвоить ей "Значение").

Раз уж была упомянута косвенная адресация, то просто нельзя не поговорить о приеме, хорошо знакомом программистам, использующим C и C++. Речь пойдет об указателях. А именно -- об указателях на функции. Совсем необязательно знать C-подобные языки для того, чтобы понять, чем удобен такой тип данных. Если множество операций с неким объектом (типом данных и т. д.) формируется набором функций, отличающихся только именами и "внутренностями", но не "интерфейсами", то бывает очень удобно использовать для вызова конкретной функции из этого множества не ее имя, а числовой индекс. Такое, естественно, возможно, только если в языке есть тип данных "указатель на функцию" и в программе создан массив из объектов этого типа, в каждом из которых записан адрес соответствующей функции. В Tcl никаких указателей нет и в помине. Но между тем подобный прием более чем применим, и вы уже знаете все необходимое для того, чтобы реализовать его аналогию (пусть пока примитивную). Давайте вспомним тот факт, что целью работы интерпретатора Tcl является формирование наборов вида "имя_команды параметр1 параметр2 ...". Добавим к нему еще пару известных фактов -- интерпретатор не "подразумевает" никакой семантики в формируемых наборах и не делает никаких различий между их элементами. А это значит, что трюковые приемы, которые мы использовали в элементах-параметрах наборов, никто не запрещает применять в элементе "имя_команды". Вот простейший интерактивный пример создания "указателей на функции без указателей на функции" (все действия выполняются в консоли tkcons, каждая строка завершается нажатием Enter):

set A clear
set B ls
set Var B
[set [set Var]]

Заметьте, что произошло после ввода последней строки -- выполнилась команда "ls". Хотя для ее вызова использован косвенный механизм: в переменной Var на момент запуска на исполнение последней строки примера записано имя переменной B, в которой, в свою очередь, содержится строка "ls" (интерпретатор и не подозревает, что это может быть именем команды). Но в результате подстановок "[set [set Var]]" трансформируется сначала в "[set B]" и затем -- в "ls". Это-то значение интерпретатор и передает диспетчеру. А так как команда "ls" находится последней -- она и выполняется. В данном случае Var играет роль индексной переменной -- ею можно "адресовать" массив имен (который в Tcl также реализуем).

Давайте завершим рассмотрение двух механизмов подстановок одной хорошей Tcl-шуткой. Только вначале вспомним, что если инструментальное средство нечто в принципе допускает, совершенно не обязательно это нечто делать с его помощью (микроскопом -- забивать гвозди, астролябиями -- измерять что угодно, было бы что мерить). Итак, "самая косвенная реализация операции присваивания" (как обычно -- интерактивный режим консоли tkcons, каждая строка завершается нажатием Enter):

set OpCode_Container A
set Operand_Container B
set Value_Container C
set A set
set B Var_1
set C Value
[set [set OpCode_Container]] [set [set Operand_Container]] [set [set Value-Container]]

"Красотища" выполняет, на первый взгляд, простую операцию -- создает переменную с именем Var_1 и присваивает ей значение "Value". Но есть одно "но" -- операция выполняется только при указанных значениях переменных B и C. А вот если все это оформить в виде фрагмента подпрограммы и значения B и C изменять извне (что можно делать, но об этом -- далее)... то мы фактически написали упрощенный шаблон генератора локальных (в подпрограмме) переменных с именами и значениями, указываемыми из другой подпрограммы. Здесь интересна "идиосинкразическая" конструкция "set A set", апофеозом которой является легендарная и абсолютно легальная "set set set" (создать переменную с именем set и присвоить ей значение "set"). К слову, она подчеркивает одно неявное, но "святое" правило Tcl, отличающее этот язык от многих других: "операция присваивания -- всего лишь одна из множества равноправных функций, находящихся в распоряжении диспетчера, а не встроенная операция интерпретатора".

Теперь можно приступать к изучению последнего (третьего) механизма подстановки -- "с обратным слешем" (МПОС). МПОС, надо сказать, самый "неказистый" механизм в коротком перечне. "Неказистый" потому, что в нем элементарен базовый принцип, а сложности кроются в исключениях. Базовый принцип МПОС был проанализирован в предыдущей части статьи (разве что мы не говорили о нем как о механизме подстановки), но давайте его еще раз повторим -- как только интерпретатор "наталкивается" на пару символов "\нечто", он подменяет эту пару символом "нечто", утратившим свое специальное значение. Ранее мы уже рассматривали несколько "диковинный" пример с "подавлением" специального значения символа "пробел" -- команду типа "set\ A 1", но в контексте обсуждаемой тогда модели "интерпретатор Tcl как аналог машины Тьюринга". Теперь можно взглянуть на этот пример с другой точки зрения -- в нем интерпретатор производит замену пары символов "\пробел-разделитель" на символ "просто_пробел". Уже очевидно, что если в программе вам нужен, например, символ "[", то его надо предварять обратным слешем -- "\[": в этом случае пара "\квадратная_скобка_инициатор_МКП" будет заменена символом "просто_квадратная_скобка". Правило распространяется на все специальные символы Tcl, включая "непечатные" (пробел и символ новой строки) и собственно символ "\".

А теперь о неинтересном -- исключениях из правила. Пары \a,\b,\f,\n,\r,\t,\w обрабатываются иным образом -- вместо них подставляются соответствующие коды ASCII (звукового сигнала, сдвига на позицию влево и т. д. -- все это есть в документации). Последовательности вида "\DDD" (D-цифра в восьмеричной системе счисления, т. е. от 0 до 7, количество цифр -- от одной до трех), "\xHH" (H -- цифра в шестнадцатеричной системе счисления, от 0 до F, количество цифр -- две) и "\uHHHH" (H -- цифра в шестнадцатеричной системе счисления, от 0 до F, количество цифр -- четыре) заменяются механизмом МПОС на Unicode-символ с кодом DDD/HH/HHHH соответственно.

И напоследок -- самое "противное" исключение, чуть ли не разрушающее всю стройную картину Tcl... Ничто в мире не идеально, увы -- и для последовательности "\ символ-разделитель_новой_строки символ-разделитель_пробел" (пробелы в этом словесном описании, естественно, игнорируйте -- они использованы исключительно для облегчения чтения) в Tcl даже предусмотрен отдельный "предпроход" интерпретатора по исходному тексту программы, о котором мы еще не говорили. То есть -- еще до перевода машины Tcl в состояние группировки (о нем -- в предыдущей части статьи) она "пробегает" по тексту программы и заменяет везде такие последовательности на один пробел. Впрочем, давайте попробуем восстановить попранную стройность аналогии и будем говорить о замене такой последовательности как об отдельном механизме, а не об исключении из МПОС. Тогда выделение слова "везде" становится понятным -- раз "машина Tcl" еще не переведена даже в состояние группировки, значит, ни о каких специальных символах она "не имеет представления". Следовательно, замены "противных последовательностей" будут произведены действительно везде -- в тексте, заключенном в фигурные скобки, и т. д.


И это все?

Как ни странно, в синтаксической части интерпретатора Tcl -- это действительно все (скажем так -- почти все... на "вообще все" нашего журнала явно не хватит). И естественно, сам собой напрашивается наивный вопрос -- "и что же можно с этим делать?". "С этим" -- практически ничего, если не вспомнить, как расшифровывается аббревиатура Tcl. Tool Command Language -- это все-таки язык управления инструментами. А инструменты Tcl -- команды. И без их изучения знание синтаксиса, тем более -- почти несуществующего, так же "помогает в быту", как знание фундаментальных принципов работы ядра ОС помогает быстро освоить современную пользовательскую операционную систему и начать эффективно работать со сложными высокоуровневыми приложениями. Иначе говоря -- никак.

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


Все только начинается

Знакомство с командами Tcl мы начнем несколько неожиданно. Естественно, в силу интерактивного характера использованных примеров это не столь очевидно, но... дотошный читатель должен был заметить, что даже в третьей части наших бесед ни разу не прозвучало хорошо знакомое каждому программисту слово "комментарий". В любой знаменитой программе класса "Hello, world!" комментарии непременно используются, а автор умудрился до сих пор их даже не упомянуть. И тому есть одна-единственная причина: в отличие от большинства языков, в которых комментарий -- конструкция сугубо синтаксическая и посему игнорируемая механизмами разбора, в Tcl-- это... инструмент. Или команда -- если вам так больше нравится. И "имя" этой команды -- "#". Именно так -- один символ. Выполняемые командой действия просты -- она принимает переменное число параметров и... ничего не делает. А теперь стоит задуматься, что сие может означать...

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

...
# относительно "безболезненный" комментарий
...

Во-вторых, понятно, что без использования символа разделителя команд ";" (о нем мы говорили еще в первой части статьи) располагать в одной строке код команды и комментарий нельзя. Поэтому можете считать пару символов ";#" еще одной Tcl-идиомой: хотя между ними можно поставить сколько угодно пробелов или табуляторов, в Tcl принято писать именно так:

...
set LSinV 299792458;# скорость света в вакууме
...

В-третьих, на основе ранее оговоренного понятно, что без ухищрений команда комментария "работает" только до конца строки.

В-четвертых, раз комментарий -- команда, следовательно, вы можете создать собственный синтаксис для комментариев (причем делается это элементарно).

И наконец, в-пятых. Мы пока практически не упоминали о программной "кухне", обеспечивающей работу Tcl-системы. И подробно говорить не будем. Но одно важное отступление все же сделаем. Tcl уже довольно давно не чисто интерпретируемый язык, превращающий непосредственно поток символов исходного текста в "поток исполнения". Современный Tcl основан на трансляции программы в промежуточное представление (байт-код). Соответственно у каждой операции на уровне байт-кода имеется своя "цена", выражающаяся в затрачиваемых на ее выполнение ресурсах. Для стандартной команды комментария "#" есть свой байт-код с практически нулевым значением "цены". Но если вы создадите свою команду комментария -- ее "цена" будет хоть и незначительной, но все-таки далеко не нулевой.

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

Но для использования set предусмотрен еще и третий формат, о котором мы пока не говорили. И прежде чем мы перейдем к его рассмотрению (так как он открывает фактически новую тему), давайте еще раз повторим пройденное на следующем примере:

set A 5
set B +2
set C “[set A][set B]”

Проверьте в консоли tkcon -- переменной C присвоено значение “5+2” (ни в коем случае не 7, а именно последовательность символов "5+2"). Причем в формировании значения C участвовал только интерпретатор Tcl (это очень важно понимать) -- последняя команда set после фаз группировки и подстановки получила в качестве параметров значения "C" и "5+2".

Давайте еще раз четко определим грань, отделяющую интерпретатор Tcl от диспетчера команд и подчиненного ему инструментального набора. Задача интерпретатора -- "нарезать" из входной "ленты" (строки исходного текста) методами группировки и подстановки упорядоченное множество "кусочков". Диспетчер использует только первый элемент этого множества -- первый "кусочек" и, считая его именем команды, ищет в инструментальном наборе одноименный инструмент. Остальные "кусочки" диспетчера не интересуют -- их обрабатывает инструмент (команда). У каждой команды могут быть (и есть) свои требования к синтаксису содержимого "кусочков". И изучение инструментального набора Tcl как раз и заключается в одновременном тщательном рассмотрении синтаксиса "кусочков" (параметров), необходимых для каждой команды, и описании функции (или функций), выполняемой командой. Это правило следует неукоснительно соблюдать при самостоятельной работе с Tcl.