learnxinyminutes-docs/ru/tcl.md
2024-12-08 23:20:53 -07:00

580 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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)