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