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

25 KiB
Raw Permalink Blame History

contributors translators filename
Poor Yorick
https://pooryorick.com/
Viktor Sokhranov
https://github.com/weirdvic
learntcl-ru.tcl

Tcl был создан Джоном Оустерхаутом в качестве скриптового языка в своих инструментах проектирования электрических цепей. В 1997 году за разработку языка Tcl автор получил 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.

#! /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

Tcl Wiki

Tcl на Reddit