Что если существует язык — точнее, целый инструментарий, — который:

  • подходит для создания кроссплатформенных консольных и GUI-инструментов;
  • даёт GUI-приложениям нативный вид на Windows, Linux и macOS;
  • поддерживает безопасную и эффективную многопоточность, неблокирующие события и ввод-вывод;
  • занимает около 100 МБ с большинством стандартных пакетов;
  • умеет собирать очень компактные однофайловые приложения под любую поддерживаемую платформу;
  • способен создавать полноценные кроссплатформенные веб-приложения;
  • используется крупнейшими корпорациями уже более трёх десятилетий;
  • бесплатен и распространяется по BSD-лицензии, так что с ним можно делать что угодно?

Звучит неправдоподобно?

indiana-jones-tcl-tk-hero-title

Предисловие

Разработчик, ведущий блог cgicoffee.com, давно документирует личный опыт по интересным или полезным темам: рендеринг в реальном времени, 3D-симуляция, безопасность данных, энергоэффективность, устройства ввода, микроконтроллеры, фитнес — список тем для изучения не заканчивается.

Тема Tcl/Tk оказалась куда объёмнее, чем планировалось изначально. Автор обещает: вся информация здесь — то, что он сам хотел бы знать до начала знакомства с Tcl/Tk.

Текст написан спустя долгое время после завершения «медового месяца» с Tcl/Tk, поэтому автор старается быть максимально объективным и честным — как перед читателем, так и перед собой. Ничего «продавать» он не собирается, кроме личного опыта, с целью упростить процесс знакомства для тех, кто решит попробовать Tcl/Tk.

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

Важно: базовые понятия программирования здесь не объясняются — предполагается, что читатель уже знаком с логикой программирования вообще. Если понятны конструкция if и понятие функции, можно сразу переходить к специфике синтаксиса Tcl. Материал задуман и как введение, и как справочник, чтобы помочь оценить почти сорокалетнюю технологию, которая тихо работает по всему миру — и, возможно, убедить попробовать её самостоятельно.

Приступим.

Введение

Среди изобретений, изменивших человечество, — печатный станок, электричество, транзистор. Из интеллектуального достояния — Linux, протоколы Всемирной паутины, концепция криптографии с открытым ключом. То, что обычно воспринимается как данность или вовсе остаётся незамеченным.

Tcl, или «Tool Command Language», созданный Джоном Аустерхаутом (John Ousterhout) в 1990 году, заслуживает места среди величайших продуктов человеческого разума — особенно в паре с более известным графическим тулкитом Tk. В 1997 году Аустерхаут получил награду ACM Software System Award за Tcl/Tk — премию, вручаемую за создание программных систем с долгосрочным влиянием на индустрию.

Хорошее общее описание Tcl можно найти в репозитории исходного кода проекта:

Tcl предоставляет мощную платформу для создания интеграционных приложений, связывающих разнородные приложения, протоколы, устройства и фреймворки. В паре с тулкитом Tk Tcl даёт самый быстрый и мощный способ создавать GUI-приложения для ПК, Unix и macOS. Tcl также применяется для веб-задач и создания командных языков для приложений.

Tcl поддерживается, развивается и распространяется свободно сообществом Tcl. Разработка исходного кода и отслеживание багов ведётся на core.tcl-lang.org. Релизы Tcl/Tk и списки рассылки размещены на SourceForge, а Tcl Developer Xchange — на www.tcl-lang.org.

Tcl — свободно доступный открытый пакет. С ним можно делать практически всё что угодно: изменять, распространять, продавать целиком или частями. Подробности — в файле "license.terms".

Хорошо, но что всё это значит, и почему это должно быть интересно?

Почему это важно

В какой-то момент возникла задача разработать кроссплатформенное desktop-приложение с графическим интерфейсом для Windows и Linux (X11/Wayland). До этого годами использовался AutoHotkey для небольших утилит под Windows.

AutoHotkey, особенно версия v2, весьма функционален и подходит для разработки небольших GUI-инструментов, а не только «хоткейных» задач. С улучшенным «C-подобным» синтаксисом им приятно пользоваться при опыте работы с JavaScript, PHP или C#.

Единственная проблема — он работает только в Windows. Да, через слой совместимости Wine скрипты AHK и однофайловые исполняемые файлы можно запускать в Linux, но нет гарантии, что все специфичные для Windows привязки и вызовы нативных библиотек, на которые опирается AutoHotkey, будут работать корректно или вообще будут работать.

В поиске кроссплатформенного GUI-фреймворка или рантайма были рассмотрены и другие варианты:

  • C# с WinForms — экосистема .NET рассматривалась в первую очередь, но WinForms фундаментально привязана к Windows API. Даже с современными кроссплатформенными возможностями .NET для действительно нативного вида на Linux или macOS нужно мигрировать на MAUI или Avalonia, а оба варианта несут огромный объём зависимостей и мучительны для разработки «некорпоративного» софта
  • Headless локальный веб-сервер (подход «как в Electron») — рассматривался и вариант с современной разделённой архитектурой: headless-бэкенд с локальным HTTP API и фронтендом на основе браузера. Такой подход обеспечивает почти 100% совместимость с любой ОС и де-факто стал стандартом для современных приложений, но такая сложность — колоссальный оверкилл для небольшого инструмента. Пришлось бы управлять коммуникацией фронтенда и бэкенда, конфликтами портов и накладными расходами на безопасность — ради того, чтобы отрисовать одну кнопку внутри вкладки браузера, потребляющей более 100 МБ просто для пустой страницы
  • Go (Golang) с Fyne/Gio — Go даёт эффективные бинарники, но опыт разработки с доступными UI-библиотеками быстро превращается в «ад зависимостей». Простое окно «Hello World» подтянуло 40 000 косвенных зависимостей! А итоговый статический бинарник превысил 30 МБ и совершенно не выглядел как «нативное приложение»
  • Qt (через C++ или Python) — конечно, рассматривался и Qt, «индустриальный стандарт». Увы, сложность его мета-объектного компилятора (MOC), путаница сигналов и слотов через нативные границы, а также запутанная модель лицензирования (GPL/LGPL против коммерческой) сделали его неприемлемым

qt-confusing-licensing

Стало понятно, что современные «кроссплатформенные» решения для GUI-приложений не являются кроссплатформенными за счёт переносимости. Вместо этого они просто упаковывают всё своё окружение, куда бы ни требовалось запускаться, компилируясь под целевую комбинацию ОС/процессора. Они не задействуют вызовы нативного UI API операционной системы, а «рисуют пиксели на холсте», из-за чего такие интерфейсы редко выглядят нативно.

Должен был существовать способ лучше!

Именно тогда, по счастливой случайности, обнаружилось упоминание некоего «тулкита Tk», и началось погружение в кроличью нору удивительных открытий…

Знакомство с Tcl/Tk

Прежде чем читать дальше, рекомендуется потратить 40 минут на просмотр видеообзора Tcl — он вводит в курс истории и текущего состояния языка, не перегружая эту и без того объёмную статью. Не нужно вникать во все примеры кода в видео — достаточно уловить общую идею, когда, зачем и как появился Tcl.

Если коротко: Tcl задумывался как универсальный «архитектурный клей», связывающий высокопроизводительный скомпилированный код с гибким, читаемым слоем логики.

В программировании «клеевой язык» — это язык программирования, используемый для соединения, управления и автоматизации отдельных, уже существующих программных компонентов, изначально не предназначенных для совместной работы. Вместо создания основной логики с нуля, «клей» используется, чтобы:

  1. Связать: подключить высокоуровневый пользовательский интерфейс к низкоуровневому, высокопроизводительному коду (например, C++ или Rust)
  2. Организовать: управлять потоком данных между разными программами или модулями
  3. Обернуть: предоставить простую скриптовую команду для запуска сложного процесса

«Клеевой» код не выполняет тяжёлые вычисления сам, а лишь координирует компоненты, которые это делают.

Изначально Tcl был реализован исключительно как язык программирования со своим интерпретатором высокого уровня, но Аустерхаут быстро понял, что консольные приложения имеют ограничения в плане взаимодействия с пользователем, поэтому язык был расширен Tk — кроссплатформенным тулкитом графического интерфейса. Отсюда и название «Tcl/Tk», которое используется повсеместно, поскольку Tk стал очень популярен и стал одной из ключевых причин использовать Tcl вообще.

Так как Tk является core-частью Tcl, чтобы превратить консольное приложение в GUI, достаточно «запросить» пакет Tk. Вуаля! Нативно выглядящий графический интерфейс собирается буквально несколькими строками кода.

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

tcl-tk-tclsh-and-wish

Отношение Tk к Tcl можно сравнить с отношением Unity Engine/Unreal Engine к OpenGL/Vulkan. Большинство разработчиков игр пишут в биографии «я Unity-разработчик», а не «я OpenGL-разработчик». Слой абстракции (в случае Tcl — расширение Tk) — вот где живут ценность и сообщество, тогда как базовый язык становится специализированной «деталью реализации». Разработчик визуальных эффектов, конечно, должен понимать основы низкоуровневых API OpenGL или Vulkan, но с высокой вероятностью 80% его кода будет взаимодействовать с высокоуровневыми абстрагированными API игрового движка.

Tcl также семантически простой язык с очень хорошо написанной C-реализацией. Именно поэтому он доступен на большинстве платформ: Windows, Linux, macOS, а также на Android через Termux для CLI-приложений на Tcl, либо через мобильные приложения Androwish (Android) и iWish (iOS) для запуска GUI-скриптов — с поддержкой различных архитектур процессоров: ARM, x86, RISC-V и т.д.

Сам по себе Tcl не представляет собой ничего особенного как интерпретируемый язык по сравнению с аналогами. Хотя есть черты, выделяющие Tcl: событийная философия, гомоиконичность, подход «всё есть строка» (о них пойдёт речь далее) — это не единственный способ писать функциональный софт. Более того, именно эта крайняя гибкость Tcl, где код является данными, а данные — кодом, усложняет его понимание для современного разработчика по сравнению с более «строгими/структурированными» языками вроде JavaScript, Python или даже Lua и Perl.

Так где же Tcl проявлял и до сих пор проявляет себя лучше всего?

«Герои не носят плащей»

Не стоит опасаться заявлений в духе «мир программирования несправедливо забыл про Tcl, хотя это лучшая вещь на свете!»

Дело в том, что Tcl просто… есть.

Это самая ироничная его черта. Будучи одним из наименее «популярных» языков, почти 40 лет он используется повсюду: в критически важных системах, в высококлассном сетевом оборудовании, в оркестрации транзакций крупнейших банков. Поскольку Tcl очень компактен (ядро написано на C), он встроен в вещи, которыми все пользуются ежедневно, даже не подозревая об этом:

  • Git: инструменты git gui и gitk, поставляемые с каждой установкой Git, — это Tcl/Tk-приложения. Выглядят они «старомодно», поскольку используют классические виджеты Tk, но при этом почти «неубиваемы» и работают на любой ОС без зависимостей
  • FPGA/чипы: почти каждый крупный инструмент проектирования аппаратного обеспечения (Xilinx, Altera, Cadence) использует Tcl как основной язык автоматизации
  • Сетевое оборудование: в Cisco IOS встроен интерпретатор Tcl для скриптов Embedded Event Manager прямо в маршрутизаторах
  • Intel, NVIDIA и AMD: инженеры этих компаний используют Tcl/Tk для внутренних GUI, управляющих массивными симуляционными фермами и тестерами оборудования
  • Siemens EDA (бывшая Mentor Graphics): их многомиллионные программные пакеты (Calibre, Virtuoso) используют Tcl как основной способ написания пользовательских скриптов для взаимодействия с GUI и аппаратными моделями
  • ESA: Европейское космическое агентство и связанные с ним аэрокосмические партнёры — масштабные пользователи Tcl/Tk. В крупных европейских аэрокосмических лабораториях, вроде ESTEC в Нидерландах или ESOC в Германии, Tcl/Tk-приложения работают в фоне критически важных операций

Tcl встречается везде, где надёжность, обратная совместимость и кроссплатформенность стоят на первом месте.

Удивительно? Автор тоже удивился. С Tcl/Tk можно писать CLI и GUI-инструменты, которые надёжно работают на всём — от терминала атомной электростанции до Raspberry Pi — без необходимости что-либо перекомпилировать. Разве не хочется получить доступ к такому мощному инструменту бесплатно, без всяких условий?

Связь с SQLite

Стоит упомянуть, что Tcl и SQLite были созданы одним и тем же сообществом ярких умов, в которое входит Ричард Хипп (Richard Hipp) — автор SQLite, самой распространённой и используемой в мире базы данных.

Фактически SQLite изначально создавался как расширение Tcl! Есть доклад самого Хиппа, где он объясняет, как появился SQLite и почему одной из главных причин его успеха стало именно «tcl-прошлое». Очень интересный доклад, рекомендуется к просмотру.

sqlite-tcl-extension

Несколько интересных фактов о SQLite и Tcl:

  • Половина всех тестов SQLite написана на Tcl
  • Третья, текущая версия SQLite использует объекты с двойным представлением («dual-ported objects») при работе — по аналогии с Tcl (концепция будет объяснена далее в статье)
  • Tcl используется как «ассемблер» для сборки финального кода SQLite: он берёт более 125 исходных C-файлов, серьёзно их постобрабатывает и генерирует итоговый исходный код объёмом свыше 200 тысяч строк
  • Разработчики SQLite — милые, замечательные люди. Вот находка при просмотре объединённого файла sqlite.c:

bless-you-sqlite

Анатомия Tcl/Tk

Как и почти любой другой язык, Tcl может распространяться разными способами и в комплекте с разными пакетами. Функциональную установку Tcl/Tk составляют следующие компоненты:

  • tclsh — от «tickle shell» — «чистый» консольный интерпретатор Tcl. Может запускаться из любой консоли целевой ОС и является основной «рабочей лошадкой» языка
  • wish — от «window shell» — интерпретатор Tcl, стартующий сразу с загруженным пакетом тулкита Tk, предназначен для разработки и запуска GUI-приложений. В Linux и macOS запуск оконной оболочки Tcl из консоли открывает стандартное окно Tk и запускает «цикл событий» в фоне (об этом позже), тогда как в Windows он скомпилирован именно как «оконное приложение Windows»
  • Библиотеки Tcl/Tk — они же Tcllib и Tklib. Набор стандартных библиотек, которые должны идти в комплекте с определённой версией Tcl/Tk, предоставляя вне-ядерные функции и расширенные возможности языка и графического тулкита. Содержимое дистрибутива может различаться, но обычно библиотека содержит такие важные расширения, как http, csv, htmlparse, json, aes и многие другие для самого Tcl, а также мощные дополнительные виджеты для Tk: tooltip или widget — набор «мега-виджетов», включающий dateentry (выбор даты в календаре), scrolledwindow и dialog
  • Расширения и модули («пакеты») — расширяют Tcl новой функциональностью: многопоточностью, drag-and-drop, поддержкой SSL. Обычно поставляются вместе с дистрибутивом Tcl/Tk, либо устанавливаются вручную, либо создаются и распространяются между приложениями по мере необходимости. Расширения могут быть реализованы на чистом Tcl или использовать нативно скомпилированные библиотеки для более глубокой интеграции с API целевой ОС

Именно такая простота, а также осторожный подход к обновлениям от версии к версии, делают Tcl чемпионом обратной совместимости. Скрипт Tcl/Tk, написанный в 1995 году, зачастую прекрасно работает и сегодня, особенно если использует только чистые команды Tcl или Tk.

Всё это доступно бесплатно благодаря BSD-модели лицензирования Tcl. Стоит подчеркнуть, что именно означает такая лицензия для Tcl и всех, кто им пользуется:

  • Tcl — навсегда. Теперь это общее «благо». Он «принадлежит» всем
  • С ним можно делать почти всё. И благодаря «геологическим слоям» почти 40 лет инженерной работы, проделанной одними из умнейших людей планеты, он до сих пор обеспечивает отличную кроссплатформенную интеграцию desktop GUI. Более удобного инструментария для разработки инструментов буквально не найти — особенно учитывая, насколько компактным он может быть. Настолько компактным, что Tcl долгие годы оставался одним из самых встраиваемых языков для инструментов
  • Недавно выпущенный Tcl 9.0 — мост, который сохранит это 40-летнее наследие жизнеспособным ещё на 40 лет 64-битных вычислений с активным использованием Unicode. Тем, кто причисляет себя к «продвинутым пользователям», нет ни одной причины не изучить хотя бы основы Tcl

Основы Tcl

Вопреки распространённому мнению, Tcl — не просто «тот странный устаревший командный язык с инопланетным синтаксисом».

tcl-programmers-shamans

Код Tcl выглядит как слова, разделённые табуляцией или пробелами. Эти слова можно вводить напрямую либо генерировать с помощью команд и процедур, которые выдают другие слова, и так далее — при этом всегда соблюдается одно главное правило:

Первое слово всегда — команда.

За ней следует ноль или более аргументов, формирующих полную командную строку:

Команда Аргумент Аргумент Аргумент …

Что касается синтаксиса — язык подчиняется 12 базовым правилам, часто называемым «додекалогом» (dodecalugue).

Ключевые основы Tcl:

  • setустановка/получение значения переменной. При вызове с 2 аргументами — создаёт (при необходимости) и устанавливает значение переменной: set myStr "Time and Date". С 1 аргументом — получает значение переменной: set myStr; # => вернёт "Time and Date"
  • $подстановка переменной. Заменяет имя переменной её значением. Функционально $ — просто «синтаксический сахар» для команды set, поэтому оба варианта позволяют получить значение переменной
  • procопределение процедуры. Базовая «функция», выполняющаяся в собственном изолированном стеке, как и в большинстве других языков. С нюансом: в Tcl procэто на самом деле команда, создающая другую команду! Так, proc sayHi {name} {return "Hi, $name!"} создаёт команду sayHi с указанными параметрами и телом
  • []подстановка команды. Выполняет вложенный скрипт и заменяет скобки результатом этого скрипта. В терминах C — это встроенный вызов функции, возвращающий значение
  • {}группировка (буквальная). Группирует слова в единый аргумент без каких-либо подстановок. Всё, кроме обратного слэша \ внутри, трактуется как сырая строка
  • ""группировка (с подстановками). В этой группировке $, [] и \ обрабатываются интерпретатором, и их содержимое заменяется результатами такой обработки/подстановки
set myStr "Time and Date"
# This would execute the commands within [] brackets
# and replace the value of the '$myStr' variable with a previously defined value
puts "$myStr: [clock format [clock seconds]]"
# So the final string received by the 'puts' command would look something like this: 
# => "Time and Date: Thu Mar 26 11:40:24 +0000 2026"
  • \подстановка обратным слэшем. Экранирует специальные символы или продолжает строку. Полезно для разбиения длинной командной строки на несколько строк — буквально экранируя символ новой строки, следующий за слэшем
  • ::оператор области видимости пространства имён. Как префикс указывает на глобальную область видимости (например, ::myVar). Как разделитель — определяет путь к команде или переменной внутри иерархии пространств имён (например, ::Namespace::Command)
  • {*}раскрытие аргументов. Трактует один список как несколько отдельных аргументов. Хотя данные в Tcl — строка, эту строку можно интерпретировать как целое «предложение» и как список отдельных слов, разделённых пробелами. Второе нужно, например, если требуется передать эти данные как список или как команду с аргументами в Tcl. Случаи использования разберём позже
  • \n или ;разделители команд. Позволяют разделять команды переносом строки или точкой с запятой на одной строке — так что скрипты можно писать и форматировать как угодно, без произвольных ограничений
  • #комментарий. Tcl действительно поддерживает комментарии, но они всегда должны начинаться в позиции, где может начинаться команда. # и всё до конца логической строки игнорируется. Но это не вся история: поскольку Tcl проверяет комментарии после разбора команды на слова, но до подстановок, фигурные скобки { должны быть парными даже внутри комментариев. Иначе Tcl будет искать закрывающие скобки } через несколько строк и не выполнит код (или выдаст ошибку, в зависимости от режима работы), пока все закрывающие скобки не будут предоставлены. Это просто особенность Tcl, следствие его синтаксического единообразия, о которой должен знать любой Tcl-разработчик

Пара примеров, демонстрирующих командные строки Tcl:

# Command 'concat' followed by two arguments
concat {Hello } "World!"
# => returns "Hello World!"
# This is also a command followed by arguments, with the difference that
# 'string' is an 'ensemble' command, which acts as a 'command dispatcher'.
# It looks at the first argument 'range' to determine which internal function to execute
# Think of it as a C-like 'String.range(str,start,end)'* or Git's 'git commit -m "Done"'
string range "Tcl Programming" 0 2 ; # => returns "Tcl"

Пример string range хорошо демонстрирует функциональную природу «чистого Tcl»: строка не «просит саму себя обрезаться» — вместо этого вызывается инструмент «range» внутри ансамбля «string», чтобы обрезать переданные данные. Инструменты для объектно-ориентированных паттернов тоже доступны — это будет рассмотрено далее в статье.

Какой интерпретатор использовать — tclsh или wish?

Оба вида интерпретатора Tcl поддерживают 2 режима работы:

  • Интерактивный режим — при запуске tclsh или wish без аргументов интерпретатор входит в интерактивный режим работы, также называемый Read-Eval-Print Loop (REPL). Он выполняет команды, введённые пользователем в консоли, и выводит результат. Это похоже на то, как работает сама консольная оболочка (bash в Linux, PowerShell в Windows), и предназначено для экспериментов и мгновенной обратной связи. Технически в этом режиме можно запускать и целые скрипты — вводя построчно, вставляя весь скрипт в консоль, или используя команду source для загрузки и интерпретации файла с кодом Tcl, — но есть правильный способ сделать это:
  • Пакетный режим (режим скрипта) — предназначен для выполнения и автоматизации, активируется запуском tclsh или wish с именем файла скрипта — tclsh myscript.tcl. В этом режиме интерпретатор читает весь файл от начала до конца, выполняет команды неинтерактивно и завершает работу после завершения скрипта. Это стандартный способ развёртывания готовых приложений, автоматизации CLI-задач или запуска GUI-программ, аналогично многим другим интерпретаторам и CLI-приложениям, принимающим параметры командной строки. Поэтому, чтобы запустить примеры скриптов из статьи, их нужно сохранить как файлы и передать интерпретатору, например: wish mortgage_calculator.tkapp

Хотя расширение файла для интерпретатора не имеет значения — важно только содержимое файла, — рекомендуется придерживаться стандартных соглашений. Использовать .tcl для большинства скриптов или следовать специфическим ассоциациям, как в дистрибутиве Magicsplat для Windows, чтобы различать CLI и GUI-скрипты (.tclapp или .tkapp).

Что касается выбора интерпретатора — есть некоторые «нюансные» различия между ориентированным на CLI tclsh и ориентированным на GUI wish.

Например, в Windows интерпретатор wish скомпилирован как «Windows GUI Application» и не может взаимодействовать с консолью. Чтобы предоставить её разработчику, при запуске показывается «псевдо-консоль», известная как «Tk Console». Технически это означает, что запускать всё можно и через wish, но есть существенная оговорка: UI и логика скрипта живут в одном потоке. Если скрипт выполняет тяжёлые вычисления, он «морит голодом» GUI, отбирая время простоя, нужное для перерисовки. Из-за этого операторы puts фактически ставятся в очередь и не отображаются в псевдо-консоли, пока скрипт не приостановится или не завершится, оставляя окно консоли «замороженным» тем временем.

Напротив, интерпретатор tclsh скомпилирован как «Windows Console Application», и поэтому интегрируется в консоль (cmd или PowerShell). Так что если запустить тот же скрипт с tclsh, все сообщения puts будут выводиться в консоль одно за другим, поскольку Windows действительно предоставляет консольным приложениям стандартные каналы ввода и вывода — stdin и stdout. Именно поэтому системная консоль надёжнее принимает и отображает данные, отправленные в эти каналы, между «блокирующими поток» циклами вычислений.

Проще говоря: при изучении Tcl/Tk используйте tclsh:

  • он всегда предоставит скриптам стандартные каналы ввода и вывода независимо от ОС — Windows, Linux или macOS, — упрощая разработку кроссплатформенных инструментов
  • для разработки GUI-инструментов — tclsh автоматически загрузит пакет Tk GUI и создаст окно верхнего уровня по умолчанию, если оно когда-либо запрашивается в скрипте через package require Tk, позволяя практиковаться в создании GUI-приложений, когда будет удобно
  • то же самое при изучении цикла событий Tcl — достаточно загрузить пакет Tk, чтобы запустить фоновый цикл событий и поработать с событиями файлов и каналов прямо в интерактивной сессии. Технически это возможно и в консольном tclsh с помощью кастомной событийной реализации REPL, но об этом не стоит беспокоиться на раннем этапе

Гомоиконичность и крайняя гибкость Tcl

Поскольку код Tcl состоит из «просто слов», для самого языка это означает, что Tcl не проводит стену между кодом и данными и позволяет самому разработчику решать, как интерпретировать ту или иную строку. Её можно трактовать как данные — как абзац текста или список элементов, — а можно изменить на лету и передать ту же строку интерпретатору как командную строку для выполнения в качестве кода.

Такая концепция называется «гомоиконичностью».

В информатике язык является гомоиконичным (от греческого homo- «тот же самый» и eikon «образ»), когда структура программы идентична структуре данных. В случае Tcl и код, и данные — строки, и нет разграничения между «ключевыми словами» и «функциями».

hold-up

В предисловии обещалась полная честность. Так вот:

В половине книг про Tcl эта «гомоиконичность» на этом моменте превозносится как что-то удивительное, гениальное. Или делаются возмутительные заявления вроде: «Это меняет всё!!!11»

Это весьма спорно. Утверждать, что «данные как код» — это преимущество, всё равно что строить дом, где каждый кирпич — потенциальная шашка динамита. Это архитектурный кошмар для всех, кто ценит структурную целостность и безопасность.

Простая демонстрация того, как строку можно интерпретировать и как данные, и как код, и как легко здесь ошибиться:

# Set or receive a string with data
set userinput {puts "Hello World!"}
# Treat it as a list of items, and count them
llength $userinput; # => returns '2', those being 'puts' and {Hello World!}
# Now interpret the same string as a complete *command string* instead
# Expand the contents with {*} to make Tcl interpret grouped words as list items
# This way Tcl will see a separate command 'puts' with an argument 'Hello World!'
{*}$userinput; # => returns "Hello World!"
# Now let's 'forget' to properly group data with quotes
# A simple typo in 'data' becomes a CRASH in 'code'
set userinput {puts Hello World!}
# Now 'puts' will see 2 arguments, treating the first one as a channel name,
# and try to send the string 'World!' to the "Hello" channel
{*}$userinput; # => ERROR: can not find channel named "Hello"

Обратите внимание, как содержимое строки — «просто слова», разделённые пробелом, а значит, первое слово всегда трактуется как вызов команды, а остальное — как аргументы. Нужно аккуратно группировать аргументы, содержащие пробелы, внутри командной строки. Это одна из причин, почему более структурированные языки со временем в целом вытеснили подобные подходы. Поскольку они чётко изолируют код программы от данных, приходится специально форматировать нечто как вызов функции, чтобы это выполнилось как валидный код: console.log("Hello World!"). Именно такая жёсткость делает возможным надёжный статический анализ кода.

Впрочем, можно ли интерпретировать случайную строку как функцию в JavaScript? — Конечно! Для этого используется eval:

let cmd = 'console.log("Hello World!")';
eval(cmd);
// => logs "Hello World!"

Опытные разработчики уже морщатся при упоминании eval, зная, насколько опасно считать «динамическое» вычисление кода хорошей идеей.

На самом деле гомоиконичность — «побочный эффект» синтаксиса Tcl. Это не какая-то «магическая» или «гениальная» функция. Если главное правило — разделённые пробелами команды и аргументы, а единственные механизмы группировки — фигурные скобки {} и кавычки "", то код неотличим от списка слов. Вот и всё. Это структурная неизбежность.

Так что нет, Tcl не «удивителен» вследствие своей гомоиконичной природы. Он скорее чрезвычайно податлив.

Поэтому далее — более «безопасный» и реалистичный пример того, как гомоиконичность Tcl может пригодиться.

Простая демонстрация гомоиконичности Tcl

В C# или JavaScript while — зарезервированное слово, встроенное в грамматику компилятора. Но поскольку в Tcl всё подчиняется схеме команда аргумент аргумент ..., даже такие «базовые» команды, как if, for или while, — просто команды, ничем не отличающиеся от команды, которую вы сами написали бы для вывода текста или перемещения файла.

Взглянем на цикл while в Tcl:

set x 0
while {$x < 3} {
    puts "Iteration $x"
    incr x
}

Здесь while — просто команда, за которой следуют 2 аргумента:

  • {$x < 3} — аргумент-«условие», проверяемое на каждой итерации
  • {puts "Iteration $x" ; incr x} — скрипт, выполняемый на каждой итерации цикла

По базовым правилам синтаксиса фигурные скобки {} сообщают интерпретатору, что содержимое нужно трактовать как буквальную строку и просто передать команде while. Таким образом, if, for, while и прочие команды просто принимают {некоторые данные} в качестве аргументов для работы. Это ещё раз доказывает, что в Tcl даже «дефолтные ключевые слова» на самом деле не ключевые слова, а команды, реализованные разработчиками языка так же, как это мог бы сделать любой разработчик Tcl-скриптов.

В противоположность этому, в JS/Python/C# и многих C-подобных языках существует «жёсткая стена» между написанным кодом (синтаксисом) и строками/числами, с которыми код работает (данными). Там if, while и for — действительно ключевые слова. Например, в исходном коде JS для движка V8 (Chrome/Node.js) ключевое слово while обрабатывается огромным парсером на C++ и конечным автоматом. Это «статичный» код, данный разработчику для использования «как есть».

Хорошо... А что если нужна собственная версия цикла while?

Чтобы сделать свой while в JavaScript, придётся обернуть код в функции, чтобы предотвратить его немедленное выполнение. Это потому, что в JavaScript x < 10 — выражение. Если не обернуть его в стрелочную функцию () =>, оно вычислится в true или false ещё до того, как достигнет вашей функции. Технически это возможно, но выглядит некрасиво.

Взгляните на этот беспорядок:

function myWhile(conditionFn, bodyFn) {
    while(conditionFn()) { bodyFn(); }
}
// You MUST use "() =>" (Lambdas) to "freeze" the code
myWhile(() => x < 10, () => { console.log(x); x++; });

В Tcl же собственную процедуру можно написать очень просто — она может принимать две строки, переданные в {фигурных скобках}, и работать с ними.

Нужно лишь не выполнять переданный скрипт-условие в изолированной области видимости новой процедуры, а использовать команду uplevel, чтобы «дотянуться» до переменных, указанных в этих фигурных скобках, из области видимости той процедуры, которая вызвала реализацию цикла. По сути это тот же способ, каким работают «дефолтные» if, for, while и многие другие команды Tcl.

Например, напишем команду repeat, принимающую 2 строки и поддерживающую 2 режима работы: until и while:

proc repeat {body mode condition} {
    while {1} {
        # IMPORTANT! Execute the body in the *caller's* scope
        uplevel 1 $body
        # Now evaluate the condition string in the caller's scope as well
        set result [uplevel 1 [list expr $condition]]
        # Switch logic based on the 'mode' provided, easy to extend
        switch -exact -- $mode {
            "until" {if {$result}  { break }}
            "while" {if {!$result} { break }}
            default {
                return -code error "Unknown operator '$mode': must be 'until' or 'while'"
            }
        }
    }
}
# Usage: "until"
set x 0; puts "Testing 'until' x >= 3:"
repeat {puts "x is $x"; incr x} until {$x >= 3}
# Usage: "while"
set y 0; puts "\nTesting 'while' y less than 3"
repeat {puts "y is $y"; incr y} while {$y < 3} 

Вуаля! Готова новая версия «дефолтной» команды while в Tcl.

Это похоже на то, как под капотом работает стандартная команда if, только с другим порядком аргументов:

set x 7
# command argument argument argument argument
if {$x > 10} {puts "High"} else {puts "Low"}
# You can even generate 'else' dynamically, since it's just an argument string
if {$x > 10} {puts "High"} [string cat "el" "se"] {puts "Low"}

Даже return — не зарезервированное слово. Оно не «зашито» в компилятор для остановки выполнения, как в C++, C# и JS, а является просто командой, напрямую взаимодействующей с интерпретатором через целочисленные коды:

  • 0 (TCL_OK)
  • 1 (TCL_ERROR)
  • 2 (TCL_RETURN)
  • 3 (TCL_BREAK)
  • 4 (TCL_CONTINUE)

Любой из этих кодов можно указать вручную при вызове команды return, меняя поведение Tcl по своему усмотрению.

Слово для опытных Tcl-разработчиков

angry-tom-tcl-developer

Этот короткий раздел адресован опытным Tcl-разработчикам, которые дочитали до сюда и могут негодовать после критики гомоиконичности Tcl. Новички могут спокойно пропустить его.

Разумеется, известно о существовании безопасных интерпретаторов Tcl (safe interpreters). Известно и то, что колбэки нужно оборачивать в [list]. Наконец, в маршрутизаторах Cisco, например, политики EEM (Embedded Event Manager) по сути являются списками строк и прекрасно работают со строковым Tcl. Но, насколько известно, Cisco выбрала Tcl прежде всего потому, что это лёгкий, строковый движок, позволяющий трактовать пользовательский текст как логику аппаратного уровня, идеально вписываясь в жёсткие ограничения по памяти железа 90-х. Ну и ещё Expect. Кто знает — тот знает. Помимо Tcl, других вариантов для реализации гибкого пользовательского доступа к системным API просто не было.

Зависимость Cisco от Tcl превратилась в состояние поддержки унаследованного технического долга. Компания находится в процессе масштабного архитектурного разворота, и именно опасность строковой природы Tcl, где «что угодно может быть чем угодно», — причина поиска альтернатив. Вместо прямого взаимодействия Tcl-скриптов с плоскостью управления на C, Cisco теперь поощряет запуск Python 3 внутри контейнера Guest Shell, поскольку он не трактует строки как логику аппаратного уровня по умолчанию, а взаимодействует с маршрутизатором через структурированные API (вроде cli.execute() или моделей NETCONF/YANG), а не через сырое вычисление строк.

Гомоиконичность Tcl была архитектурным сокращением пути, которое упростило встраивание языка в 90-х, но делает его менее предпочтительным сегодня, когда всё больше внимания уделяется безопасности, даже ценой более строгих правил и структур. Мир современных сетей слишком велик и дик, чтобы игнорировать риски широкого использования гомоиконичного языка. Современные процессоры и статические анализаторы теперь гораздо лучше умеют оптимизировать и обеспечивать безопасность жёстко структурированных языков. Эти инструменты снижают когнитивную нагрузку на разработчиков, обнаруживая ошибки и уязвимости гораздо раньше в цикле разработки. Это страховочная сетка, которую такой гибкий язык, как Tcl, просто не может предоставить.

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

Если кто-то не согласен — автор готов выслушать аргументы напрямую через страницу «About» блога.

Язык с «другой философией власти»

Внимание — данные вовсе не обязательно трактовать как код, если только сам разработчик этого не выберет! Инструменты и GUI можно разрабатывать, используя лишь «дефолтный» набор команд и расширений Tcl/Tk. Но дверь всегда открыта. В C#, JavaScript, Python и подобных языках такая власть принадлежит команде компилятора в Microsoft, Mozilla или Python Software Foundation. В Tcl власть принадлежит тому, кто пишет скрипт.

Гомоиконичность Tcl поначалу покажется «странной» и порой запутанной, особенно если есть большой опыт с более «жёсткими» или «структурированными» языками. Но со временем она станет понятнее, и её даже можно обратить себе на пользу. Например — для создания целых наборов пользовательских команд, операторов и потоков управления в виде предметно-ориентированных языков (DSL).

По мере накопления опыта с Tcl постепенно приходит понимание «Tcl-образа мышления», и естественным образом внимание смещается к изначальной цели этого «инструментального командного языка»: от разработки инструментов, скриптования и автоматизации — к «склеиванию» компонентов приложения между собой. Именно здесь Tcl/Tk по-настоящему раскрывается, предоставляя портативный, нативно выглядящий интерфейс для управления высокопроизводительными, нативно скомпилированными бинарниками, выполняющими тяжёлую работу. Мостик между этими двумя мирами будет рассмотрен далее в статье.

Tcl/Tk — живее всех живых с выходом Tcl 9.0

Хотя и не слишком «популярный», Tcl/Tk по-прежнему активно развивается основной командой разработки Tcl/Tk. 13 ноября 2025 года, спустя 12 лет после выхода Tcl 8.6, была представлена новая мажорная версия — Tcl/Tk 9.0.

Это по-настоящему знаковый релиз, поскольку он сделал Tcl полностью 64-битно-осведомлённым языком. Может показаться странным и даже забавным, почему это не было сделано раньше, но уже можно догадаться, что несёт такой переход: размер указателя меняется с 32 на 64 бита. Это значит, что некоторые существующие Tcl-скрипты, особенно встраивающие чистый C-код (пакет critcl), могут перестать работать или вести себя непредсказуемо. И поскольку Tcl исторически обеспечивал выдающуюся обратную совместимость, основной команде разработчиков пришлось быть очень осторожной при внедрении такого обновления, сохраняя совместимость с как можно большей частью существующей, иногда десятилетиями назад написанной кодовой базы. Это заняло время.

С этим обновлением Tcl стал ещё более «современным» и надёжным, чем прежде:

  • Поддерживает полный диапазон кодовых точек Unicode. Tcl и раньше был известен превосходной поддержкой локализации («интернационализации»), а новый релиз упорядочивает его юникод-основу, ещё больше улучшая ведущую в индустрии поддержку многоязычных инструментов:

tcl-unicode-support

  • Tcl теперь нативно поддерживает виртуальную файловую систему ZipFS. После многих лет зависимости от сторонних хаков и продуктов для создания самодостаточных приложений или упаковки данных со скриптами, теперь можно прикрепить zip-файл к скрипту и смонтировать его как виртуальную точку доступа к файлам и каталогам внутри. Об этой важной возможности подробнее — далее в статье, в разделе про Tcl 9 и ZipFS
  • Tcl/Tk 9.0 значительно улучшил возможности графического интерфейса (GUI), добавив поддержку масштабируемой векторной графики (SVG), а также сильно улучшив осведомлённость Tk о высокой плотности пикселей (high DPI), что будет продемонстрировано далее. Также Tk 9.0 добавил нативную поддержку уведомлений рабочего стола и лучшую интеграцию с системными темами вроде тёмного режима на Windows/macOS
  • Санитизация восьмеричных литералов по умолчанию — в Tcl 8.x 010 интерпретировалось как восьмеричное число (8), что приводило к бесконечным багам «что за математика?!» при обработке чисел с ведущими нулями, вроде почтовых индексов или дат. Tcl 9 использует префикс 0o для восьмеричных чисел, например 0o10. Ведущий ноль больше не меняет систему счисления числа. Если старые скрипты полагаются на прежнее поведение, теперь они будут трактовать 010 как десятичное 10 — одно из тех очень желанных, но потенциально ломающих обратную совместимость изменений
  • В целом более строгая обработка данных. Tcl 8.x иногда пытался «быть умным» при чтении текстовых файлов, молча заменяя некорректные последовательности байтов символами-заменителями или откатываясь на ISO-8859-1 (Latin-1) при ошибках кодировки. Хотя это предотвращало падение скриптов, часто это приводило к неожиданной порче данных, которую почти невозможно было отладить после сохранения. В Tcl 9.0 это поведение заменено формальной системой Encoding Profile. Так, если в Tcl 8.x при чтении файла как UTF-8 попадался посторонний не-UTF-8 байт, Tcl часто просто продолжал работу, тогда как профиль по умолчанию в Tcl 9.0 теперь strict. Если Tcl встречает некорректную последовательность байтов для указанной кодировки, он сразу же выбрасывает ошибку, что критически важно для разработки кроссплатформенных инструментов. За последний год пришлось иметь дело с множеством произвольно закодированных файлов, и Tcl, сразу сообщая о проблеме с файлом, оказался весьма кстати — с файлом, который, кстати, C# и Python сочли вполне нормальным, поскольку «быть умным» — особенность обоих. Кратко: Tcl 8.x предполагал, что программист хочет, чтобы программа продолжала работать любой ценой. Tcl 9.0 предполагает, что программист хочет, чтобы данные были корректны любой ценой.

Хотя использовать Tcl 9.0 не обязательно и можно по-прежнему разрабатывать CLI и GUI-инструменты на ветке Tcl 8.6, рекомендуется либо начинать сразу с версии 9.0, либо обновиться до неё. Это даёт множество преимуществ, дополнительные страховки при обработке данных и значительно улучшенные возможности UI в Tk.

Наконец, официальный исходный код Tcl/Tk — не единственный способ пользоваться языком. Другие разработчики предлагают собственные интерпретаторы с минимальным количеством C-кода:

  • Jim TCL — компактная реализация языка программирования Tcl, реализующая большинство возможностей Tcl в очень компактном интерпретаторе размером около 100-200 КБ. Всё это — менее чем на 10 тыс. строк C-кода, с множеством доступных расширений
  • Или, в крайних случаях, есть проекты вроде Picol — где менее чем 1000 строк C-кода позволили автору воспроизвести синтаксис Tcl и некоторые ключевые команды языка

are-you-done-yet

Хорошо, хватит разговоров о «чистом Tcl».

Как ни интересна тема Tcl сама по себе, есть одна черта тулкита Tcl/Tk настолько повсеместная и универсальная, что о её связи с Tcl часто забывают:

Тулкит Tk GUI

Не будем ходить вокруг да около и перейдём к ключевой причине, почему Tcl особенно актуален сегодня, как и на протяжении последних десятилетий — тулкит Tk. Вскоре после первого публичного релиза Tcl Джон Аустерхаут осознал, что мир вычислений стремительно движется в сторону более удобных графических пользовательских интерфейсов. Он также знал, что существует несколько конкурирующих операционных систем, каждая — со своей реализацией UI API. Это привело к созданию Tk, который стал неотъемлемой частью Tcl в том виде, в каком мы его знаем. Настолько неотъемлемой, что «общее имя» самого языка сменилось на Tcl/Tk.

Со временем Аустерхаут решил превратить Tk в кроссплатформенный тулкит, чтобы один и тот же код Tcl можно было использовать (с минимальными изменениями) для создания GUI на разных платформах.

retro-tk-interface

Tk — оригинальный UI «Написал один раз — запусти где угодно». Задолго до того, как Electron начал потреблять всю оперативную память мира, Tk предоставлял лёгкий мост между Windows, macOS и X11 с таким объёмом занимаемого места, из-за которого пустой бинарник на C# выглядит раздутым. Из-за этого Tk был настолько распространён в профессиональных кругах (автопром, чиповая индустрия, астрономия), что оценить, сколько интерфейсов было реализовано на Tk и до сих пор используется, совершенно бессмысленно.

gaia-tk-interface

Более того, Tk стал настолько мощным и портативным, что языки вроде Python, Perl и Ruby приняли его в качестве стандартной библиотеки — например, tkinter в Python, который будет рассмотрен позже.

Кроссплатформенность Tk даже не является его главной фишкой. Главная фишка — скорость разработки. Функциональную кроссплатформенную панель можно собрать в 100 строках Tcl, тогда как на C++/Qt на это ушло бы 1000 строк.

Однако, глядя только на скриншоты десятилетних интерфейсов Tk, можно решить, что GUI на Tk уродливы. Действительно, первые 20 лет после создания Tk не имел ttk — набора тематизированных виджетов Tk. На любой платформе он выглядел как интерфейс Windows 3.11 или 95: серый, прямоугольный, с очень спартанскими вариантами оформления. Поэтому со временем большинство разработчиков перешли на веб или Qt ещё до того, как Tk наконец обрёл нативный вид. Они не оглянулись, чтобы увидеть, что это исправлено.

Это давно исправлено, и даже более того!

Например, известно ли, что Tk поддерживает темы?

Из них alt, default, clam и classic присутствуют всегда, с дополнительными темами в зависимости от ОС и пакетов, предлагаемых дистрибутивом Tcl/Tk. В большинстве случаев стоит использовать тему «default» (не «classic»), поскольку она будет выглядеть максимально нативно на каждой из платформ, поддерживаемых Tk через прямые привязки к DLL Windows UI и API X11/Cocoa. Можно также создавать собственные темы или скачивать созданные сообществом.

С появлением набора виджетов ttk старые Tk-приложения довольно легко портировать для обновления внешнего вида. Вот пример очень старого инструмента, использующего «классический» виджет Tk — treectrl, запущенного без каких-либо изменений в Tcl/Tk 9.0:

classic-styled-tk-app

А вот тот же инструмент после портирования на тематизированный виджет Tk — ttk::treeview:

ttk-styled-tk-app

Вопреки стереотипу, приложения на Tk могут выглядеть почти так же нативно, как... собственно, нативные, сохраняя при этом кроссплатформенную совместимость.

Ниже — наглядная демонстрация.

Демонстрация кроссплатформенного GUI на Tk

Изучая демо-плагины браузера Tcl, автор наткнулся на «апплет ипотечного калькулятора». Последний раз он обновлялся в 1996 году, и тем не менее прекрасно работал под Tcl 9.0.3. Конечно, будучи настолько старым, он полагался на устаревшие приёмы: размер холста был жёстко задан, как и некоторые пороговые значения, GUI использовал менеджер геометрии pack вместо grid и опирался на классический набор виджетов Tk для полей ввода и кнопки, которые выглядят так, будто прибыли прямиком из 1996 года.

Вот он во всей красе:

mortgage-calculator-1996-original

Обновление калькулятора показалось хорошим учебным упражнением по Tcl/Tk. Одно потянуло за собой другое, и в итоге получилась почти полностью переписанная «модернизированная» версия с целым рядом изменений:

  • использует менеджер геометрии grid
  • позволяет указывать первоначальный взнос
  • реализует расширенную валидацию ввода и повторную подгонку
  • поддерживает экраны с высоким DPI, включая Android-мобильники
  • реализует изменение размера холста с ограничением частоты
  • имеет на 100% кроссплатформенно согласованную компоновку UI
  • использует пространства имён и массивы вместо неряшливой плоской структуры переменных
  • содержит множество комментариев, объясняющих отдельные моменты

💾 Скачать актуальную версию можно на странице оригинальной статьи, чтобы посмотреть, как реализовано всё вышеперечисленное.

Без комментариев и пустых строк оригинальная версия содержит 188 строк кода. Новая версия близка к этому — 250 строк, что можно считать справедливым, учитывая все новые функции и обновления.

Вот она запущена на Windows 10:

mortgage-calculator-100-perc-scale-windows

И на Linux Mint MATE 22.2:

mortgage-calculator-100-perc-scale-linux

Хорошо выглядит на Android-планшете в Androwish в портретной ориентации (масштабировано с 1600x2560):

mortgage-calculator-android-tablet-portrait

И в ландшафтной ориентации тоже:

mortgage-calculator-android-tablet-landscape

Также вписывается в узкий экран Android-телефона (масштабировано с 1080x2340):

mortgage-calculator-android-phone

Все скриншоты сделаны при запуске одного и того же скрипта.

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

А вот скрипты Tcl и Tk, запущенные на миниатюрном одноплатном компьютере на базе ARM с ОС на базе Arch Linux, показанные на 5-дюймовом экране 1024x600:

tcl-tk-on-arm-sbc

Но это ещё не всё!

Tk готов к High DPI/4K

Tcl/Tk 9.0 значительно улучшил поддержку высокого DPI по сравнению с предыдущими мажорными релизами. Теперь современная библиотека виджетов ttk:: полностью разрушает стереотип о том, что приложения на Tk выглядят «старыми» и «уродливыми» или не способны корректно адаптироваться под экраны с высоким DPI, вроде 4K.

Вот тот же «модернизированный» скрипт ипотечного калькулятора, запущенный на машине с Windows 10 и дисплеем 1080p, с системным масштабированием 100%, что соответствует плотности пикселей 96 DPI по умолчанию:

mortgage-calculator-100-perc-scale-windows

А вот он же в Windows 10 с масштабированием 150%, что соответствует 144 DPI:

mortgage-calculator-150-perc-scale-windows

И, конечно, про macOS не забыто. Вот тот же скрипт, запущенный на Mac с экраном высокого DPI. Также видно, насколько «своенравна» macOS, отказываясь масштабировать кнопку по вертикали, и как Tcl/Tk легко справляется с этим, аккуратно размещая виджет кнопки по центру:

mortgage-calculator-100-perc-scale-macos

С Tcl/Tk 9.0 удалось добиться почти идеального масштабирования 1 к 1 в обоих случаях! При правильном подходе к разработке UI GUI на Tk тоже могут сохранять внешний вид на множестве устройств независимо от плотности пикселей экрана.

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

wrong-scaling-on-high-dpi-tablet

Основы Tk

Насмотревшись на красивые картинки, стоит копнуть глубже под капот и посмотреть, как строятся интерфейсы Tk.

Будучи кроссплатформенным, набор Tk UI не предоставляет доступ к нативным вызовам Win32 DLL или внутренним механизмам Linux X11/macOS Cocoa, поэтому любое необходимое взаимодействие нужно явно прописывать в коде. К счастью, Tk предоставляет высокоуровневые абстракции, также известные как «виджеты», которые берут на себя большую часть тяжёлой работы, предоставляя разработчикам необходимые элементы управления. То, как эти виджеты вписываются в архитектуру Tcl, тоже чрезвычайно элегантно — каждый виджет Tk существует как команда! Это не статичные элементы, а в некотором роде «объекты» со своими «методами», доступными для настройки.

Что важнее всего, виджеты обычно не имеют жёстко закодированного поведения и ожидают, что разработчики сами определят привязки и колбэки. А поскольку программирование UI в Tk событийно ориентировано, как и нативные UI операционных систем, изучение Tcl/Tk даёт лучшее понимание того, как под капотом устроено программирование графических интерфейсов в десктопных операционных системах.

Что касается основ Tk, уже существует ресурс по Tk с отличным туториалом. Стоит заглянуть на TkDocs.com и пройти хотя бы первые 4 главы, чтобы разобраться в специфической схеме именования с предшествующей «.» (точкой), используемой в Tk, и в том, как в целом устроены окна и виджеты. Это практически обязательное условие для понимания остальных примеров по Tk в статье.

tkdocs-good-shit

Если коротко: чтобы что-то в UI как-то реагировало на события и ввод — например, менялось состояние элементов, происходила прокрутка колёсиком мыши/тачпадом, или изменялся размер виджетов вместе с окном, — нужно определить такое поведение и привязать виджеты и события к колбэкам. Большинство таких поведений общие и хорошо задокументированы, с множеством демо и примеров.

Кстати о демо — рассмотрим пример из демо виджетов Tcl 9.0.3:

treeview-scrollbars

Здесь виден виджет ttk::treeview с 2 полосами прокрутки (комментарии добавлены для ясности):

# 'treeview' widget created as a child element of .mclist window
ttk::treeview .mclist.tree -columns {country capital currency} -show headings \
    -yscroll [list .mclist.vsb set] -xscroll [list .mclist.hsb set]
# Two scrollbars simply created as siblings of the treeview widget
ttk::scrollbar .mclist.vsb -orient vertical -command [list .mclist.tree yview]
ttk::scrollbar .mclist.hsb -orient horizontal -command [list .mclist.tree xview]

Первое, что можно заметить: две полосы прокрутки не «встроены» в виджет treeview. Автор демо решил сделать их «сиблингами» виджета, как это часто делается. Это значит, что виджет treeview можно снабдить всего одной или вовсе ни одной полосой прокрутки, если нужно, и вместо этого управлять им, например, стрелками клавиатуры или чисто программной логикой (например, функцией поиска, автоматически прокручивающей к результату). Но как только в окно добавляются дополнительные элементы управления, им как-то нужно взаимодействовать со скроллируемыми виджетами.

Разберёмся, как это работает под капотом:

  1. От виджета к полосе прокрутки: когда содержимое внутри treeview меняется — колёсиком мыши или вставкой данных, — виджет выполняет командную строку, назначенную опции -yscroll, в данном случае — .mclist.vsb set. Он добавляет две дроби, представляющие видимый диапазон, командуя полосе прокрутки обновить размер и положение своего ползунка. И, конечно, .mclist.vsb сам по себе — команда, созданная ttk::treeview или ttk::scrollbar, которые тоже являются командами
  2. От полосы прокрутки к виджету: когда пользователь взаимодействует с полосой прокрутки, она получает командный префикс, назначенный опции -command. Для вертикальной прокрутки это .mclist.tree yview. В зависимости от того, как пользователь взаимодействовал с полосой прокрутки, она передаёт команде yview соответствующие аргументы прокрутки, например moveto 0.5 или scroll 1 units. Иными словами, полоса прокрутки берёт команду .mclist.tree yview и добавляет к ней аргументы, например: lappend command "moveto" 0.5, получая полную командную строку {.mclist.tree yview "moveto" 0.5}, которую затем выполняет (примерно как eval $command), давая treeview команду сдвинуть внутреннюю систему координат на новую позицию

Видно, что Tcl ничего не скрывает. Он буквально строит командные строки и выполняет их как указано. Это значит, что можно как воссоздать «нативное» поведение элементов управления окном, так и заставить любые элементы управлять любым числом других элементов, выстраивая портативный, кроссплатформенный GUI. Большинство современных фреймворков вроде Electron, Flutter или SwiftUI прячут эту логику «рукопожатия» за реактивным состоянием или привязкой данных, действуя как «чёрные ящики», сдерживая рост разработчиков и приводя ко всевозможным трудноуловимым багам и проблемам производительности. Не то чтобы в Tcl-скриптах не будет багов, но в подавляющем большинстве случаев это будет полностью ваша вина, легко диагностируемая и исправляемая. Очень освежающе!

Также обратите внимание, как колбэки -command в примере передаются в виде списков. Это считается хорошей практикой при построении колбэков для виджетов Tk или команд Tcl вроде eval, after, bind, subst, поскольку элементы [list …] «атомарны». В Tcl это предотвращает ошибки разбиения на слова, если аргументы колбэка вдруг содержат пробелы или спецсимволы. В таких языках, как JS, передаётся ссылка на функцию с аргументами: setTimeout(myFunc, 1000). В Tcl передаётся строка, которую интерпретатор разберёт и выполнит позже. Разработчику нужно самостоятельно убедиться, что на момент вызова эта строка будет содержать валидную командную строку, где команда и все аргументы правильно сгруппированы. Список автоматически заключает содержимое с пробелами в фигурные скобки {}, сохраняя нужный порядок и количество элементов. Это одна из тех «tcl-измов», к которым со временем привыкаешь, — упомянутый выше «побочный эффект» принципа «всё есть строка» и общего синтаксиса Tcl, где команды и аргументы разделены пробелом.

Чтобы лучше понять «листинг колбэков», попробуйте выполнить эти команды и понаблюдать за результатом:

# Load in Tk to ensure the Tcl event loop is running
package require Tk
# Create data with spaces
set data "Data with spaces!"
# Set up a callback that FAILS, because $data is replaced with the value,
# and the resulting command and arguments are NOT properly grouped
after 1000 "puts $data"; # Sets up a callback 'puts Data with spaces!'
# ERROR: can not find channel named "Data"
# Callback that SUCCEEDS, because [list] will automatically brace the string
# that contains spaces or special chars, generating a valid command call
after 1000 [list puts $data]; # Sets up a callback 'puts {Data with spaces!}'
# SUCCESS: "Data with spaces!"

Что касается взаимодействия виджетов, такая жёсткая связка хорошо работает для тесно связанных элементов (например, полоса прокрутки в окне, или массив чекбоксов на холсте). Однако для настройки связи между системами стоит поискать другой подход. Строить UI с отдельными окнами и контекстами с плотной связью через параметры -command нормально для небольших инструментов или во время изучения Tk. Но как только программа вырастает за пределы 2-3 окон, управлять такими зависимостями становится заметно сложнее.

К счастью, есть элегантное и надёжное решение.

Tcl/Tk — данные-ориентированный и событийный по замыслу

american-psycho-patrick-bateman

Именно здесь Tcl/Tk по-настоящему раскрывается, и именно поэтому на демонстрацию того, как тулкит может стать «лучшим другом менеджера» — мощным кроссплатформенным программным «клеем», надёжно связывающим системы и компоненты, ушло почти полгода написания этого материала.

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

eventloop-from-tkdocs

Проще говоря, цикл событий Tcl непрерывно обрабатывает события, извлекаемые из очереди событий, обычно десятки раз в секунду. Он следит за событиями мыши и клавиатуры, вызывая колбэки команд и привязки событий…

(материал продолжается в оригинальной статье — рассматриваются потоки, корутины, Starkits/Zipkits, веб-приложения на Wapp, ограничения языка и рекомендации по установке Tcl/Tk для разных платформ)