IRC Info > Статьи > Почему я не использую egglib
На главную

Почему я не использую egglib

Я могу сколько угодно говорить о стиле написания TCL скриптов для eggdrop, их оптимизации и тому подобном, но в большинстве случаев это будет встречено словами «А у меня работает! Белого шерифа проблемы негров не волнуют!». Я, конечно, не претендую на истину в последней инстанции и каждый случай для реализации того или иного скрипта/программы должен рассматриваться отдельно, не существует чётких и общих правил написания программ. Но, увы, существуют общие распространённые ошибки.

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

1. Злоупотребление regexp/regsub.

Мне откровенно непонятна такая любовь к regexp/regsub авторов egglib да и писателей скриптов в целом. Использовать regexp/regsub надо только там, где это действительно необходимо, либо просто элегантно. Но, использовать regsub для замены одного символа на другой в строке это забивание гвоздей микроскопом. Есть хорошая и красивая функция string map, которая делает эту работу намного быстрей (реально раз в 5-6 быстрей аналога regsub). Таких злоупотреблений в egglib больше чем предостаточно.

1.1 Ненужные функции egglib (или имеющие более короткие аналоги)

Вот они, кандидаты на оптимизацию или удаление:

  • ::egglib::toupper — преобразование строки к верхнему регистру
  • ::egglib::tolower — преобразование строки к нижнему регистру

Посмотрим на них поближе… О боже! Что мы видим! (даже не хочется приводить их полностью) Для каждого символа написан regsub!!!

proc ::egglib::tolower {t} {
  regsub -all -- {А} $t {а} t; regsub -all -- {Б} $t {б} t; regsub -all -- {В} $t {в} t
  regsub -all -- {Г} $t {г} t; regsub -all -- {Д} $t {д} t; regsub -all -- {Е} $t {е} t
  ...
  return $t
}

Ну и ради чего этот огород?!! Ну хорошо, пока не будем вдаваться в подробности, просто используем string map который в качестве параметров принимает список вида: key value ?key value?. Иными словами каждая такая пара описывает «что заменять» «чем заменять». Запишем это намного короче и элегантней:

proc ::noegglib::tolower { str } {
  set lclist [list А а Б б В в Г г Д д Е е Ё ё Ж ж З з И и К к Л л М м Н н О о П п Р р С с Т т У у Ф ф Х х Ц ц Ч ч Ш ш Щ щ Э э Ю ю Я я \
                   A a B b C c D d E e F f G g H h I i J j K k L l M m N n O o P p Q q R r S s T t U u V v W w X x Y y Z z]
  return [string map $lclist $str]
}

Но тут же может возникнуть вопрос: «А string tolower/toupper отменили женевской конвенцией?». Почему бы не использовать их?! Тут конечно можно мне возразить, мол «это не будет работать для бота без патча (скажем патча Suzi project) или для кодировки iso8859-1». А вот ничего подобного! Будет работать, и работать прекрасно. Простым добавлением encoding converto/convertfrom (отчего и почему возникают проблемы с национальными символами в TCL скриптах eggdrop и как их решать можно подробней прочитать в моей статье «Eggdrop и нелатинские символы»).

А давайте проверим для эксперимента быстродействие. Хотя во многих случаях оно не играет решающей роли (всё-таки поток данных eggdrop небольшой), но зато мы наглядно увидим разницу между regsub и string map для одиночных символов.

namespace eval egglib {}
namespace eval noegglib {}

# Оригинальная функция из egglib
proc ::egglib::tolower {t} {
    regsub -all -- {А} $t {а} t; regsub -all -- {Б} $t {б} t; regsub -all -- {В} $t {в} t
    regsub -all -- {Г} $t {г} t; regsub -all -- {Д} $t {д} t; regsub -all -- {Е} $t {е} t
    regsub -all -- {Ё} $t {ё} t; regsub -all -- {Ж} $t {ж} t; regsub -all -- {З} $t {з} t
    regsub -all -- {И} $t {и} t; regsub -all -- {Й} $t {й} t; regsub -all -- {К} $t {к} t
    regsub -all -- {Л} $t {л} t; regsub -all -- {М} $t {м} t; regsub -all -- {Н} $t {н} t
    regsub -all -- {О} $t {о} t; regsub -all -- {П} $t {п} t; regsub -all -- {Р} $t {р} t
    regsub -all -- {С} $t {с} t; regsub -all -- {Т} $t {т} t; regsub -all -- {У} $t {у} t
    regsub -all -- {Ф} $t {ф} t; regsub -all -- {Х} $t {х} t; regsub -all -- {Ц} $t {ц} t
    regsub -all -- {Ч} $t {ч} t; regsub -all -- {Ш} $t {ш} t; regsub -all -- {Щ} $t {щ} t
    regsub -all -- {Ъ} $t {ъ} t; regsub -all -- {Ы} $t {ы} t; regsub -all -- {Ь} $t {ь} t
    regsub -all -- {Э} $t {э} t; regsub -all -- {Ю} $t {ю} t; regsub -all -- {Я} $t {я} t
    
    regsub -all -- {A} $t {a} t; regsub -all -- {B} $t {b} t; regsub -all -- {C} $t {c} t
    regsub -all -- {D} $t {d} t; regsub -all -- {E} $t {e} t; regsub -all -- {F} $t {f} t
    regsub -all -- {G} $t {g} t; regsub -all -- {H} $t {h} t; regsub -all -- {I} $t {i} t
    regsub -all -- {J} $t {j} t; regsub -all -- {K} $t {k} t; regsub -all -- {L} $t {l} t
    regsub -all -- {M} $t {m} t; regsub -all -- {N} $t {n} t; regsub -all -- {O} $t {o} t
    regsub -all -- {P} $t {p} t; regsub -all -- {Q} $t {q} t; regsub -all -- {R} $t {r} t
    regsub -all -- {S} $t {s} t; regsub -all -- {T} $t {t} t; regsub -all -- {U} $t {u} t
    regsub -all -- {V} $t {v} t; regsub -all -- {W} $t {w} t; regsub -all -- {X} $t {x} t
    regsub -all -- {Y} $t {y} t; regsub -all -- {Z} $t {z} t

    return $t
}

# Используем string map
proc ::noegglib::tolower { str } {
  set lclist [list А а Б б В в Г г Д д Е е Ё ё Ж ж З з И и \
                    К к Л л М м Н н О о П п Р р С с Т т У у \
                    Ф ф Х х Ц ц Ч ч Ш ш Щ щ Э э Ю ю Я я \
                    A a B b C c D d E e F f G g H h I i J j \
                    K k L l M m N n O o P p Q q R r S s T t \
                    U u V v W w X x Y y Z z]
  return [string map $lclist $str]
}

# Достаточно длинная строка
set str [string repeat "АаБвГдЕеЁёЖжЗзИиКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЭэЮюЯя" 1000]

puts "TCL $::tcl_patchLevel"

# Считаем время выполнения для ::egglib::tolower
puts -nonewline "::egglib::tolower\t= "
puts [time { set lstr [::egglib::tolower $str] } 100]

# Считаем время выполнения для ::noegglib::tolower
puts -nonewline "::noegglib::tolower\t= "
puts [time { set lstr [::noegglib::tolower $str] } 100]

# Считаем время выполнения для встроенной TCL функции string tolower
puts -nonewline "string tolower\t\t= "
puts [time { set lstr [string tolower $str] } 100]

Вот какие результаты даёт выполнение интерпретатором TCL этого скрипта на моём компьютере:

TCL 8.4.13
::egglib::tolower       = 51260.8 microseconds per iteration
::noegglib::tolower     = 10495.43 microseconds per iteration
string tolower          = 1160.32 microseconds per iteration

Функция использующая string map работает почти в 5 раз быстрей, а string tolower почти в 50 раз!!! Даже если в string tolower добавить пару перекодировок чтобы сымитировать поведение на боте без патча это не приводит к заметным снижениям быстродействия. И это помимо элегантности и размера этих функций, последняя не ограничена кодировкой cp1251 (а точнее кодировкой, в которой набран текст скрипта).

Итак, подытожим, нужны ли нам функции в исходном виде:

  1. ::egglib::toupper — не нужна или должна быть записана с использованием encoding convertto/convertform и string toupper (для бота с патчем не нужны вызовы encoding)
  2. ::egglib::tolower — не нужна или должна быть записана с использованием encoding convertto/convertform и string tolower (для бота с патчем не нужны вызовы encoding)
  3. ::egglib::strip_colors — не нужна или должна быть записана с использованием encoding convertto/convertform и stripcodes bcru (это встроенная функция eggdrop, а для бота с патчем ещё не нужны вызовы encoding)
  4. ::egglib::to_translit, ::egglib::strip_special, ::egglib::backslash, ::egglib::colors_to_meta, ::egglib::meta_to_colors и ::egglib::optimize_colors — должны быть записана с использованием string map.

Ни одна из тех функции, которые должны быть записаны с использованием string map, не встречаются так уж часто в скриптах.

1.2 Псевдополезные функции

Разнообразно названные функции с общим заголовком ::egglib::outXX и имеющие различную функциональность, отличающуюся по этим суффиксам. Конечно, понятна попытка разработчиков неким образом стандартизировать вывод чего-то куда-то с некоторыми условиями. Но реализация неудобна на мой взгляд, элегантней для этого использовать ключи как параметры функции и одно название. К тому же ни одна из этих функции не умеет резать длинные строки на несколько коротких, чтобы укладываться в ограничение IRC серверов на длину строки (что более полезно на мой взгляд).

::egglib::unique_id — никак не оправдывает своего названия, потому что никогда ещё генератор псевдослучайных чисел не был генератором уникальных id, проще тогда уж использовать обычный счётчик, который увеличивается на 1, при каждом использовании этой функции (и опять же работал бы раз в 5 быстрее).

1.3 Потенциально небезопасные функции

Функции ::egglib::readdata и ::egglib::writedata потенциально могут приводить к исчерпанию числа файловых хендлов, потому что при возникновении ошибки при чтении/записи файла файловый хендл не будет закрыт и будет потерян, более того в функции записи нет необходимости делать цикл по всем строкам, можно просто сразу записать всё одной командой puts $fileio $data

2. Работа с http

О да, egglib содержит в себе реализацию клиента http на асинхронных сокетах.

::egglib::urlencode содержит ошибки (незначительные, но всё же)

  1. regexp -- {[A-Za-z0-9]} $n должно быть записано как regexp -- {[-._~a-zA-Z0-9]} $n (не учтены символы «дефис» («минус»), «точка», «подчёркивание» и «тильда» которые тоже не подвергаются URI-кодированию), большинство серверов лояльно относятся к этим вещам, хоть всю строку кодируй в URI без исключений, но некоторые зачастую выступают этакими «буквоедами» и сопротивляются.
  2. if { $n == "+" || $n == " " } должно быть записано как if { $n == " " } (символ «плюс» должен быть перекодирован URI).
  3. И, редко встречающийся случай, для символа перевода строки «\n» — он должен быть записан как двухбайтная последовательность URI: %0D%0A. Впрочем это видимо ни к чему, не помню чтобы это где нибудь использовалось, но, если мне не изменяет память, это должно быть именно так.
  4. Так же нет никаких предположений о том что строка будет не в однобайтной кодировке, а в unicode (а по идее это и есть правильно, потому что все строки TCL интерпретатора хранятся в unicode). И эта функция будет работать просто неверно для символов с кодом выше 127.

::egglib::unhtml — не покрывает весь диапазон html тэгов, использует «любимый» regsub к месту и без, содержит ошибки в реализации декодирования UNICODE символов вида &#num; (точнее можно сказать, что она отсутствует в принципе).

Ошибки в регэкспе анализа URL в функции ::egglib::http_get, которые потенциально могут привести к неправильному разбору нетривиальных ссылок. К тому же генерация уникального id для каждого запроса лежит на хрупких плечах пользователя.

Код ::egglib::http_cleanup содержит такое количество строк обёрнутых в catch, что появляется резонный вопрос «а в какой строчке кода авторы не сомневаются?». Хотя, можно было бы коротко и элегантно записать всю эту кучу unset:

foreach { key } [array names -glob "http,$id,*"] {
  unset egglib($key)
}

А ещё проще было бы создавать уникальный глобальный идентификатор и удалять его целиком (так, кстати, сделано в стандартном пэкэдже TCL http).

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

Но так ли нужна эта сомнительная реализация http? В требованиях egglib написано: «package require Tcl 8.3». В этой версии уже есть стандартный пэкэдж http имеющий не худшую (а зачастую лучшую) функциональность и лишённый недостатков egglib (ошибки в urlencode и др). И его легко можно использовать: «package require http 2».

Тут конечно есть подводный камень, стандартный пэкэдж http во время установки соединения работает в блокирующем режиме. Но этот факт практически незаметен. И ещё, для функции обработчика событий (например когда запрошенная страница получена) нельзя передать дополнительные параметры (custom_data в терминах egglib::http_get) напрямую в вызове ::http::geturl. Но это легко решается путём использования дополнительного ассоциативного массива, где с именем токена возвращаемого функцией ::http::geturl связывается список параметров (я использую именно этот способ). Либо несколько грубый, но действенный хак с созданием уникального идентификатора, основанный на том, что токен это не что иное как ассоциативный массив (код получается элегантней и с 99.99% вероятностью будет работать в последующих версиях пэкэджа http, но… если следовать правилам «не писать потенциально нерабочий код, зависящий от реализации внешних библиотек» — то следовать им до конца). Если интересно как работать с этим пакэджем — могу привести пример написания типичного «граббера» информации с какого-нибудь web-ресурса.

3. Эпилог

Конечно, это далеко не все ошибки и неточности в этой библиотеке, но ни в коем случае не принижаю заслуг авторов этой библиотеки, написавших множество различных и весьма полезных скриптов. Идеи замечательные и просто гениальные, но реализации, да, хромают (это моё мнение). Нужна ли мне egglib? Видимо нет. Её и без того небольшая полезность напрочь убита реализацией.

Нужна ли она Вам? Скорее да, если Вы используете скрипты, которые требуют наличие этой библиотеки (хотя большинство из них уже переписаны и имеют лучшую функциональность). Но, если Вы разработчик скриптов — учитывайте вышеописанные эффекты при использовании этой библиотеки.

Что еще посмотреть?

Автор: Suzi IRC Info.RU

Рейтинг: 8 (8 голосов)
Пожалуйста, оцените данную статью (страница перезагружена не будет).