--- language: Tcl lang: ru-ru contributors: - ["Poor Yorick", "https://pooryorick.com/"] translators: - ["Viktor Sokhranov", "https://github.com/weirdvic"] filename: learntcl-ru.tcl --- Tcl был создан [Джоном Оустерхаутом](https://ru.wikipedia.org/wiki/Оустерхаут,_Джон) в качестве скриптового языка в своих инструментах проектирования электрических цепей. В 1997 году за разработку языка Tcl автор получил [ACM](https://ru.wikipedia.org/wiki/ACM) Software System Award. Tcl может использоваться и как встраиваемый скриптовый язык, и как язык программирования общего назначения. Кроме того, он может быть использован как библиотека в программах на C, даже в случаях когда не требуется написание скриптов, поскольку Tcl может предоставить программе на C различные типы данных, такие как динамические строки, списки и хэш-таблицы. Также с помощью этой библиотеки возможно использовать форматирование строк, операции с файловой системой, работу с кодировками и динамически загружаемые библиотеки. К другим особенностям Tcl относятся: * Удобный кроссплатформенный API для работы с сетью * Поддержка виртуальной файловой системы (VFS) * Стекируемые каналы ввода-вывода * Асинхронность в ядре языка * Поддержка корутин * Простая и надёжная модель потоков выполнения Tcl имеет много общего с Lisp, но в отличие от списков, в Tcl "валютой" языка являются строки. Все значения являются строками. Список в Tcl это просто строка в определённом формате, а тело процедуры (скрипт) это ещё одна строка, а не блок. С целью увеличения производительности, интерпретатор Tcl использует кэшированные внутренние представления различных типов данных. Например, рутины (routines), работающие со списками, фактически используют внутреннее представление списков, а интерпретатор Tcl обновляет строковое представление в том случае если оно используется в скрипте. В Tcl используется подход copy-on-write, позволяющий оперировать большими объёмами данных без дополнительного оверхеда. Процедуры в Tcl автоматически компилируются в байткод, кроме случаев когда в процедуре используются динамические рутины, такие как `uplevel`, `upvar` и `trace`. Программировать на Tcl приятно. Его находят привлекательным хакеры, которым интересны Lisp, Forth или Smalltalk, а также инженеры и учёные, которым просто необходим гибкий инструмент для выполнения их задач. В Tcl языковые конструкции, включая циклы и математические операторы, представлены в виде изменяемых рутин, в отличие от других языков программирования, где они закреплены в синтаксисе, что позволяет синтаксису Tcl не мешать работать с предметной областью проекта. Синтаксис Tcl в этом смысле даже более минималистичен чем у Lisp. ```tcl #! /bin/env tclsh ############################################################################### ## 1. Рекомендации ############################################################################### # Tcl это не shell или C! Этот момент требует уточнения, поскольку привычки # написания shell-скриптов почти работают в Tcl и часто люди начинают # изучать Tcl со знанием синтаксиса других языков. Поначалу это работает, но # когда скрипты становятся сложнее, наступает фрустрация. # Фигурные скобки {} в Tcl используются не для построения блоков кода или # списков, а как механизм экранирования (quoting) для кода. Фактически в Tcl # нет ни списков, ни блоков кода. Фигурные скобки использутся для # экранирования специальных символов и потому подходят для представления # тела процедур и строк, которые должны интерпретироваться как списки. ############################################################################### ## 2. Синтаксис ############################################################################### # Скрипт состоит из команд, разделённых символами перевода строки или символами # точки с запятой. Каждая команда представляет собой вызов рутины. Первое слово # это имя вызываемой рутины, а последующие слова это аргументы. Слова разделены # пробелами. Так как каждый аргумент это слово в команде, он является строкой и # может быть неэкранирован: set part1 Sal set part2 ut; set part3 ations # символ доллара используется для подставления значения переменных: set greeting $part1$part2$part3 # Когда "set" получает только имя переменной, возвращается значение переменной: set part3 ;# Возвращает значение переменной # Содержимое квадратных скобок заменяется на результат выполнения: set greeting $part1$part2[set part3] # Встроенный таким образов скрипт может состоять из нескольких команд, но # результат подстановки определяется последней командой: set greeting $greeting[ incr i incr i incr i ] puts $greeting ;# Выведет "Salutations3" # Каждое слово в команде является строкой, включая имя рутины, поэтому # подстановки могут быть использованы и таким образом: set action pu # следующие команды эквивалентны: puts $greeting ${action}ts $greeting [set action]ts $greeting # Обратный слэш экранирует специальные символы: set amount \$16.42 # и он же используется для ввода специальных символов: puts lots\nof\n\n\n\n\n\nnewlines # Слово в фигурных скобках никак не интерпретируется и в нём не работают # никакие подстановки, за исключением экранирования закрывающей скобки: set somevar { Это литерал знака $, а это \} экранированная закрывающая скобка } # В слове внутри двойных кавычек, пробельные символы теряют своё # специальное значение: set name Neo set greeting "Hello, $name" # Имя переменной может быть любой строкой: set {first name} New # Фигурные скобки используются для доступа к переменным с составными именами: set greeting "Hello, ${first name}" # "set" всегда можно использовать вместо подстановки переменной: set greeting "Hello, [set {first name}]" # Чтобы "распаковать" список в команду используется оператор расширения "{*}" # Эти две команды эквивалентны: set name Neo set {*}{name Neo} # Массив это особая переменная, являющаяся контейнером для других переменных. set person(name) Neo set person(destiny) {The One} set greeting "Hello, $person(name)" # "variable" может быть использована для объявления или установки переменных. # В отличие от "set", которая использует глобальное и локальное пространство # имён, "variable" работает только с локальным пространством: variable name New # "namespace eval" создаёт новое пространство имён, если его не существует. # Пространство имён может содержать рутины и переменные: namespace eval people { namespace eval person1 { variable name Neo } } # Двумя или более двоеточиями в именах переменных отделяется название # пространства имён: namespace eval people { set greeting "Hello $person1::name" } # Два или более двоеточия также отделяют название пространства имён # в имени рутины: proc people::person1::speak {} { puts {I am The One.} } # Полные(fully-qualified) имена начинаются с двух двоеточий: set greeting "Hello $::people::person1::name" ############################################################################### ## 3. Больше никакого синтаксиса ############################################################################### # Все остальные функции реализованы посредством рутин. С этого момента и далее # больше нет нового синтаксиса. Всё остальное что можно изучить о Tcl это # поведение отдельных рутин и какие значения они присваивают своим аргументам. ############################################################################### ## 4. Переменные и пространства имён ############################################################################### # Каждая переменная и рутина связана с пространством имён. # Чтобы получить интерпретатор, который не может сделать ничего, достаточно # удалить глобальное пространство имён. Особой пользы в этом нет, но это хорошо # иллюстрирует природу Tcl. Фактически имя глобального пространства имён это # пустая строка, но единственный способ представить её -- в виде полного имени: proc delete_global_namespace {} { namespace delete :: } # Поскольку "set" всегда учитывает и глобальное, и текущее пространства имён, # более безопасно использовать "variable" чтобы объявить новую переменную или # задать значение переменной. Если переменная с именем "name" уже существует # в глобальном пространстве имён, использование "set" задаст значение # глобальной переменной, тогда как "variable" работает только с текущим # пространством имён. namespace eval people { namespace eval person1 { variable name Neo } } # После объявления переменной в пространстве имён, [set] видит её, а не # одноимённую переменную в глобальном пространстве имён: namespace eval people { namespace eval person1 { variable name set name Neo } } # Но если "set" приходится создать новую переменную, он всегда делает это # с учётом текущего пространства имён: unset name namespace eval people { namespace eval person1 { set name neo } } set people::person1::name # Абсолютное имя всегда начинается с имени глобального пространства имён, то # есть с пустой строки, за которой следует два двоеточия: set ::people::person1::name Neo # В пределах процедуры "variable" связывает перменную в текущем пространстве # имён с локальной областью видимости: namespace eval people::person1 { proc fly {} { variable name puts "$name is flying!" } } ############################################################################### ## 5. Встроенные рутины ############################################################################### # Математические операции можно выполнять при помощи "expr": set a 3 set b 4 set c [expr {$a + $b}] # Поскольку "expr" самостоятельно занимается подстановкой значений переменных, # математическое выражение нужно оборачивать в фигурные скобки чтобы отключить # подстановку значений переменных интерпретатором Tcl. # Подробнее об этом можно прочесть здесь: # "https://wiki.tcl-lang.org/page/Brace+your+expr-essions" # "expr" выполняет подстановку переменных и результатов команд: set c [expr {$a + [set b]}] # "expr" предоставляет разные математические функции: set c [expr {pow($a,$b)}] # Математические операторы сами по себе доступны в виде рутин в # пространстве имён ::tcl::mathop ::tcl::mathop::+ 5 3 # Рутины могут быть импортированы из других пространств имён: namespace import ::tcl::mathop::+ set result [+ 5 3] # Не числовые значения должны быть квотированы. Такие операторы как "eq" # Могут быть использованы чтобы провести строковое сравнение: set name Neo expr {{Bob} eq $name} # Общие операторы сравнения тоже работают со строками если числовое значение # операнда недоступно: expr {{Bob} == $name} # "proc" создаёт новые рутины: proc greet name { return "Hello, $name!" } # можно указать несколько параметров: proc greet {greeting name} { return "$greeting, $name!" } # Как было отмечено ранее, фигурные скобки не обозначают блок кода. # Любое значение, даже третий аргумент "proc", является строкой. # Предыдущая команда может быть переписана без использования фигурных скобок: proc greet greeting\ name return\ \"\$greeting,\ \$name!\" # Если последний параметр называется "args", все дополнительные аргументы, # переданные рутине, собираются в список и передаются как "args": proc fold {cmd first args} { foreach arg $args { set first [$cmd $first $arg] } return $first } fold ::tcl::mathop::* 5 3 3 ;# -> 45 # Условное выполнение тоже реализовано как рутина: if {3 > 4} { puts {This will never happen} } elseif {4 > 4} { puts {This will also never happen} } else { puts {This will always happen} } # Циклы реализованы как рутины. Первый и третий аргумент для "for" # обрабатываются как скрипты, а второй аргумент как выражение: set res 0 for {set i 0} {$i < 10} {incr i} { set res [expr {$res + $i}] } unset res # Первый аргумент для "while" тоже обрабатывается как выражение: set i 0 while {$i < 10} { incr i 2 } # Список это строка, а элементы списка разделены пробелами: set amounts 10\ 33\ 18 set amount [lindex $amounts 1] # Если элемент списка содержит пробел, его надо экранировать: set inventory {"item 1" item\ 2 {item 3}} # Хорошая практика использовать списковые рутины для обработки списков: lappend inventory {item 1} {item 2} {item 3} # Фигурные скобки и бэкслеш могут быть использованы чтобы хранить более # комплексные структуры внутри списков. Список выглядит как скрипт, за # исключением того, что перевод строки и точка с запятой теряют своё # специальное значение, а также не производится подстановка значений. # Эта особенность Tcl называется гомоиконичность # https://ru.wikipedia.org/wiki/Гомоиконичность # В приведённом списке есть три элемента: set values { one\ two {three four} five\{six } # Поскольку как и все значения, список является строкой, строковые # операции могут выполняться и над списком, с риском повреждения: set values {one two three four} set values [string map {two \{} $values] ;# $values больше не \ правильно отформатированный список # Безопасный способ работать со списками — использовать "list" рутины: set values [list one \{ three four] lappend values { } ;# добавить символ пробела как элемент в список # Использование "eval" для вычисления значения скрипта: eval { set name Neo set greeting "Hello, $name" } # Список всегда можно передать в "eval" как скрипт, содержащий одну команду: eval {set name Neo} eval [list set greeting "Hello, $name"] # Следовательно, когда используется "eval", используйте "list" чтобы собрать # необходимую команду: set command {set name} lappend command {Archibald Sorbisol} eval $command # Частая ошибка: не использовать списковые функции для построения команды: set command {set name} append command { Archibald Sorbisol} try { eval $command ;# Здесь будет ошибка, превышено количество аргументов \ к "set" в {set name Archibald Sorbisol} } on error {result eoptions} { puts [list {received an error} $result] } # Эта ошибка запросто может произойти с "subst": set replacement {Archibald Sorbisol} set command {set name $replacement} set command [subst $command] try { eval $command ;# Та же ошибка, лишние аргументы к \ {set name Archibald Sorbisol} } trap {TCL WRONGARGS} {result options} { puts [list {received another error} $result] } # "list" корректно форматирует значение для подстановки: set replacement [list {Archibald Sorbisol}] set command {set name $replacement} set command [subst $command] eval $command # "list" обычно используется для форматирования значений для подстановки в # скрипты, вот несколько примеров: # "apply" вычисляет список из двух элементов как рутину: set cmd {{greeting name} { return "$greeting, $name!" }} apply $cmd Whaddup Neo # Третий элемент может быть использован для указания пространства имён рутины: set cmd [list {greeting name} { return "$greeting, $name!" } [namespace current]] apply $cmd Whaddup Neo # "uplevel" вычисляет скрипт на уровень выше в списке вызовов: proc greet {} { uplevel {puts "$greeting, $name"} } proc set_double {varname value} { if {[string is double $value]} { uplevel [list variable $varname $value] } else { error [list {not a double} $value] } } # "upvar" связывает переменную на текущем уровне вызовов с переменной на # более высоком уровне: proc set_double {varname value} { if {[string is double $value]} { upvar 1 $varname var set var $value } else { error [list {not a double} $value] } } # Избавляемся от встроенной рутины "while" и используем "proc" чтобы написать # свою версию: rename ::while {} # обработка оставлена как упражнение: proc while {condition script} { if {[uplevel 1 [list expr $condition]]} { uplevel 1 $script tailcall [namespace which while] $condition $script } } # "coroutine" создаёт новый стек вызовов, новую рутину для входа в этот стек # и вызывает эту рутину. "yield" приостанавливает вычисления в этом стеке и # возвращает управление вызывавшему стеку: proc countdown count { # отправить что-нибудь обратно создателю корутины, фактически # останавливая стек вызовов на время. yield [info coroutine] while {$count > 1} { yield [incr count -1] } return 0 } coroutine countdown1 countdown 3 coroutine countdown2 countdown 5 puts [countdown1] ;# -> 2 puts [countdown2] ;# -> 4 puts [countdown1] ;# -> 1 puts [countdown1] ;# -> 0 catch { puts [coundown1] ;# -> invalid command name "countdown1" } cres copts puts $cres puts [countdown2] ;# -> 3 # Стеки корутин могут передавать контроль друг другу: proc pass {whom args} { return [yieldto $whom {*}$args] } coroutine a apply {{} { yield set result [pass b {please pass the salt}] puts [list got the $result] set result [pass b {please pass the pepper}] puts [list got the $result] }} coroutine b apply {{} { set request [yield] while 1 { set response [pass c $request] puts [list [info coroutine] is now yielding] set request [pass a $response] } }} coroutine c apply {{} { set request [yield] while 1 { if {[string match *salt* $request]} { set request [pass b salt] } else { set request [pass b huh?] } } }} ``` ## Ссылки [Официальная документация Tcl](https://www.tcl-lang.org) [Tcl Wiki](https://wiki.tcl-lang.org) [Tcl на Reddit](http://www.reddit.com/r/Tcl)