Мышки плакали и кололись, но продолжали есть Kokatus
Несмотря на проблемы с Koka, о которых можно почитать в первой части, я все-таки решил продолжить :/
Поддержка Юникода и сишная вставка #
Начал я с потрахушек с сишной вставкой для поддержки Юникода — она не работала из-за undefined behavior, потому что я пытался использовать ICU библиотеку для изменения строки “на месте”, а чатгпт мне врал и газлайтил про сломанное окружение. Пока сам внимательно не посмотрел на код и не подумал, что может пойти не так, ничего не получалось. Справедливости ради, сама библиотека для Юникода, такое ощущение, тоже меня газлайтила — API у нее, мягко говоря, не очень, и пару раз я получал ошибки весьма сомнительного качества, которые скорее запутывали, чем давали понять, что не так. Но после того, как сделал микро-файлик с изолированной проблемой и догадался про UB, все относительно быстро решилось. Пиковый опыт разработки на сях + пиковый опыт общения с “ИИ” помощником.
Были еще проблемы с тем, чтобы понять, что нужна статическая линковка, откуда взять библиотеки, откуда взять C++ библиотеку, можно ли как-нибудь это не руками делать и т.д.
Отдельной историей была попытка разобраться с менеджерами пакетов, conan и vcpkg, но это какая-то своя вселенная с типичным (херовым) UX, и я быстро решил эту идею свернуть, так как еще разбираться подробно с ними и их интеграцией в Koka — нет, спасибо.
Даже по вайбу — в одном тебя лягуха JFrog встречает с пикой точеной с установкой через pip
, в другом — мелкомягкие с установкой через git clone
и несколько ручных действий.
Я еще подумывал о том, чтобы поправить пулл-реквест по итогам обратной связи, которую мне все-таки дали, но решил, что покрывать все нюансы практически вслепую — так себе опыт, и отложил это дело когда-нибудь “на потом”. Тем более не могу сказать, что почувствовал четкость и уверенность в ответе — как бы потом еще раз переделывать не пришлось.
Парсер JSON #
В момент написания собственно поиска я осознал, что в ни в стандартной библиотеке, ни в библиотеке от сообщества нет парсера JSON. Только писатель.
Ладно, ладно, что-то есть, но это выглядит настолько не очень, что лучше бы его и не было.
В какой-то момент я хотел написать свой кастомный текстовый формат для сериализации, но потом все же решил написать свой мини-парсер JSON, без вещей, которые мне не нужны. При этом какие-то функции для преобразования JSON в нормальный тип в стандартной библиотеке все-таки есть, и если приправить их щепоткой магии с имплиситами, работают они неплохо.
Парсер в функциональном стиле — интересная зарядка для ума, компилятор хорошо помогает с точки зрения покрытия различных состояний. Первую версию я написал относительно легко, она была уродливой, но моей, и, что самое главное, работала.
Не очень понравилось, что нет толкового копирования структур. Есть немного сахара, однако, как я понял, это не мой случай из-за того, что у меня типы-объединения. Хотя в каком-нибудь Котлине с sealed-классом и выводом типов это сработало бы без копипасты. Позже я немного отрефакторил код и копипасты стало не так много.
Фронтенд #
Моя любимая тема:) Следующий челлендж — сделать что-то интерактивное, чтобы поиском можно было пользоваться на сайте. Для интеграции с браузером нет библиотек из коробки — ну, ничего, уже умеем делать внешние вызовы :)
Думал было побыть модным и молодежным и скомпилировать все в WASM, однако для него на кой-то ляд нужны сишные библиотеки. Честно, даже не стал вникать, решил, что
Такое ощущение, что JS для фронта был сделал по остаточному принципу. По ходу выяснились какие-то достаточно детские болячки: например, при создании библиотеки игнорируется флаг пути вывода — алло, как мне ее использовать? Или, например, если вдруг у обертки и реального вызова в JS одно имя — здравствуйте, бесконечная рекурсия.
Состояние #
Одним из проблемых вопросов, который возник почти сразу при написании морды — как хранить состояние (загруженный индекс)? Мы же в чистом ФП, при каждом запросе запрашивать и парсить JSON — отвратительно, т.е. его надо передавать как параметр к фунции. Вот только кто это будет делать и откуда? Функция по идее будет вызываться только при изменении поля ввода, т.е. вызываться браузером.
Я находил какие-то ошметки доков про то, как сделать что-то подобное в общем случае, но… ничего толком не понял, потратив кучу времени :/ Даже думал тупо забить: пусть передается вызывающим кодом, т.е. оставить небольшую обертку из JS, в которой будет изолировано все состояние и все эффекты. Задал вопрос на форуме, ни на что не надеясь, однако мне ответили быстро и по делу. Стало понятнее, но тогда я временно отложил это дело. А в итоге советами не воспользовался и выкрутился путем рефакторинга — состояние (индекс) в итоге хранится в замыкании, без дополнительных фишек языка и всего такого.
Превозмогаем fetch #
Первая попытка запуска поиска была, разумеется, неуспешая, потому что
created : .koka/v3.1.2/js-debug-4de7e1/search__main.mjs
error : command failed (exit code -11)
command: node --stack-size=100000 .koka/v3.1.2/js-debug-4de7e1/search__main.mjs
А потом еще и
...
node --stack-size=100000 .koka/v3.1.2/js-debug-4de7e1/search__main.mjs
Segmentation fault (core dumped)
В итоге всемогущий print-debug вывел меня на fetch
, который безуспешно пытался загрузить JSON-файлик с индексом.
Кажется, одна из проблем была связана с асинхронностью, поэтому я сначала решил пойти по “простому пути” и сделать вызов fetch
синхронным.
“Вертел я еще в этом зоопарке с асинхронностью разбираться”, — подумалось мне.
Разумеется, так делать нельзя, зря что ли два цвета у функций?
А синхронный метод (XMLHttpRequest
) устарел (да и выглядит уродско).
В итоге нашел адекватный способ и адаптировал его под свой случай.
Поковырялся немного, чтобы использовать голый async
и не использовать unsafe-as-string
, но это был провал.
Попробовал сделать еще нормальную ошибку при отсутствии файла.
Тут интересно, что расширяется стандартный интерфейс exception-info
:
abstract extend type exception-info
con JSError(error: jsobject<any>)
Это круто, вот только доступа к типу нет, и его нельзя никак проверить/показать, потому что он не публичный. Уже когда писал эту статью, отправил PR с исправлением, вмержили сразу.
Верстаем #
Когда разбирался с fetch
, решил попробовать просто что-то абстрактное в браузере запустить, прямо из консоли, но не тут-то было: библиотеки компилируются в модули, к функциям внутри них получить доступ из консоли — нетривиально.
Штош… придется писать SPA и генерить HTML кодом (sic!).
По умолчанию вывод main
идет в
<div id="koka-console" style="...">
<div id="koka-console-out" style="...">
<a href="https://ya.ru">test</a>
<br>
</div>
</div>
Причем с экранированием, т.е. просто выплюнуть HTML не получится. Пытался разобраться, как поменять эту обертку на что-то другое — это закопано в v1 библиотеке, которую еще и хрен подключишь.
В итоге решил, что не стоит это того, и решил через нативную функцию заменять код страницы. Это сработало. Получается, не просто будем генерить HTML кодом, но еще будем это делать в стиле реакта, когда весь HTML генериться бразуером на лету. Best practice, однако (устаревший).
Пока разбирался с изменением кода, попутно нашел и библиотеку для генерации HTML.
Вот тут (наконец-то!) смог оценить подход с эффектами.
В компонентах с их помощью собираются элементы: билдер создает эффект, а build
все “ловит” — выглядит очень круто, абсолютно ничего лишнего!
Из минусов — не очень расширяемо: например, на input
нельзя добавить атрибуты и нужно использовать более низкоуровневое API.
Однако это можно исправить, не меняя сам подход, и скорее проблема малого использования библиотеки и обратной связи для нее.
С точки зрения стилей — сначала хотел сделать все с нуля, а потом подумал: нахера изобретать велосипед?
Переделал тупо теги и классы под то, что и так у меня на сайте, и загрузил CSS оттуда ужаснейшим методом. А единственное непокрытое место, ширину поля ввода 100%, тупо сделал inline-стилем: не делать же ради этого дополнительный файл.
Интеграция с Web API #
Пробую запустить — не работает: не вызывается функция поиска.
При отладке выясняю интересную особенность — имена манглируются: do_search
→ do__search
, search-frontend
→ search_dash_frontend
.
Зачем — не очень ясно.
Было ли это проблемой?
Нет.
Потратил ли я на это время?
Да :(
Основная засада — импорты.
Наружу торчит только main
.
Попробовал привязать вызов функции к атрибуту поля ввода oninput
— мимо, потому что функция в области видимости модуля.
Ладно, попробуем через addEventListener
— тоже мимо, потому что непонятно, как пробросить название вызываемой функции правильно.
Если пробросить саму функцию — то она как будто не вызывается :(
Нашел как сделать перезагрузку скриптов и вызвал функцию напрямую, получил исключение.
Поганый JavaScript не может нормально сказать, в чем проблема :/
Ошибка оказалась где-то в std_core_hnd.mjs
— не определена переменная.
Т.е. видимо я ухожу из контекста Koka и не получается ничего сделать.
Ну, хотя бы научился редактировать скрипты в браузере.
Хотел сделать простенький репродюсер, чтобы отправить баг, а в нем все, сволочь такая, работает! Методом тыка выяснилось, что если в функции есть хоть один сайд эффект, то все, “усе пропало”. Было бы классно получить нормальную ошибку, и это минус интеграции языка с браузером.
Еще и JavaScript поднасрал своими API — потратил время на какую-то ерунду: одна и та же функция работала на кнопке, но не вызывалась на поле ввода.
Все для того, чтобы обнаружить, что правильное событие — input
, а не oninput
.
Разумеется хотя бы предупреждение в консоли показать слишком просто :/
Вдобавок есть небольшая проблема с типами: это тебе не TypeScript, в котором определил структуру типов и радуешься.
Тут определил структуру — получи [object Object]
.
Поэтому в коде остался унылый Jsobject(event).get-obj("target").get-string("value")
Все выше перечисленное чатгпт с точки зрения фронта сгенерировал бы за 1 простой промт, конечно.
Отлаживаем сам язык #
В какой-то момент словил баг компилятора:
(1, 1): internal error: unable to read
CallStack (from HasCallStack):
error, called at src/Common/Range.hs:65:53 in koka-3.1.2-2NpoSf9Uv2JEsgjfT7jQ0Q:Common.Range
readInput, called at src/Compile/Build.hs:1317:40 in koka-3.1.2-2NpoSf9Uv2JEsgjfT7jQ0Q:Compile.Build
getFileContents, called at src/Compile/Build.hs:644:17 in koka-3.1.2-2NpoSf9Uv2JEsgjfT7jQ0Q:Compile.Build
moduleLex, called at src/Compile/Build.hs:631:43 in koka-3.1.2-2NpoSf9Uv2JEsgjfT7jQ0Q:Compile.Build
(1, 0): build warning: interface async found but no corresponding source module
При этом помогало удалить .koka
.
Сузить проблему не удалось, поэтому много на нее времени не тратил.
Поборов интеграцию функции поиска с полем ввода, получил новый ворох проблем в лицо. Вот какая-то ошибка в КЧД при десереализации:
_mlift_init_10105/index__size< - search_dash_frontend.mjs:108:21
ReferenceError: $std_data_rbtree is not defined
Ну епрст, если еще и дерево хреновое, то это вообще жопа.
Обложился трейсами — теперь другая ошибка в другом месте, каеф.
string_fs_show - std_core_show.mjs:326:18
ReferenceError: $std_core_vector is not defined
В принципе “логично”, трейс выводит в консоль, функция вывода не работает — получаем ошибку. В процессе дебага увидел, что находится внутри встроенной строки — связные списки (бля, это жесть 🌚). Соответственно, работает это все не медленно, а ПИЗДЕЦ как медленно.
Ошибка с выводом поначалу пофиксилась… убиранием флага -O3
.
Не, ну я конечно понимаю, что один из таргетов — это Си, но чтобы еще undefined behaviour
из него тащить — это смело!
Оказалось еще, что даже с -O1
код работает медленнее, чем без оптимизаций вообще.
А вот фикс для работы с КЧД оказался просто конченным: добавить лишний импорт.
Это мне еще повезло, что ошибка на верхнем уровне в подконтрольном коде, иначе бы я не знал как это исправить.
Предыдущую проблему с show
это не решило.
Поборов проблему с импортами, получил следующую ошибку (я уже в шаге от MVP, честно-честно, надо чуток потерпеть…)
Paused on exception
$regexExecAll - std_text_regex.mjs:128:25
TypeError: $std_core_types._vlist is not a function
Оказалось, что проблема даже не в сгенерированном JS, а… в обычном! Это баг стандартной библиотеки языка: метод вызывается из одного модуля, а по факту находится в другом.
Проверил через перегрузку скриптов тупейший фикс — работает :/ В итоге сделал микро-PR, чтобы починить, и его приняли за полчаса, ух!
На этом баги библиотеки не кончились — Юникод нанес еще один удар, теперь по регуляркам! В сях и шарпе они юникодные, а вот в JavaScript — нет, потому что флаг не поставлен. В итоге еще один пулл-реквест, который тоже относительно быстро вмержили.
Организация кода #
До этого времени все было в одной папке и в дополнение к ней была помойка utils
.
Захотелось более четко разделить ядро, фронт и вещи, специфичные для бложика.
Тут используется хаскелловский подход к импортам.
Вроде все ок, но красиво не очень получается: либо добавлять всю папку src
, чтобы сохранить иерархию, либо добавлять используемые модули по одному, но тогда теряются префиксы.
С core
еще ладно, но вот utils
хотелось явно оставить.
В итоге оставил -isrc
, тем более это все-таки best practice, как я понял.
Получил унылое:
src/extractor/extract.kk(44,37): type error: types do not match
context : serialize(index)
term : index
inferred type: index/index
expected type: core/index/index
но в итоге порешалось использованием везде правильного префикса для импортов.
Основная логика #
Вы ведь все еще помните, что я поиск пишу, да?
В черновике заметки между пунктами выше у меня было “написал индексер”, “написал поиск” и т.п. Когда находишься в чистой абстракции, то и проблем не так уж и много. Скажем так, собственно логика поиска и “движка” была самой простой и беспроблемной задачей.
В первой итерации я сделал обычный TD-IDF.
Во второй итерации решил сделать чуть лучше — BM25.
С ним интересная оптимизация получилась: если известно k
и b
, то можно предвычислить коэффициенты.
Ради этого даже поддержку дробных числе в парсер JSON добавил.
Еще одна ошибка с импортами #
После рефакторинга получил еще одну ошибку, но теперь уже в индексаторе:
core/indexer(1, 1): internal error: Core.Parc.getDataDefInfo: cannot find type: std/data/rbtree/rbtree
CallStack (from HasCallStack):
error, called at src/Common/Failure.hs:46:12 in koka-3.1.2-2NpoSf9Uv2JEsgjfT7jQ0Q:Common.Failure
raise, called at src/Common/Failure.hs:32:5 in koka-3.1.2-2NpoSf9Uv2JEsgjfT7jQ0Q:Common.Failure
failure, called at src/Backend/C/Parc.hs:988:34 in koka-3.1.2-2NpoSf9Uv2JEsgjfT7jQ0Q:Backend.C.Parc
Failed to compile src/extractor/extract.kk
очень интересно и ничего не понятно :(
Причина оказалась во вспомогательной функции
pub fun values<k, v>(m: map<k, v>): list<v>
m.rb-map/values
Видимо, совпадающие имена в комбинации с псевдонимом типов не работали.
Возможно, из-за подобной ошибки были проблемы с импортами и в JS.
Заменить на квалифицированный .map/values
не сработало, переименовать в get-values
— тоже.
В итоге тупо переписал код, чтобы это не использовалось.
Обновление версии языка #
Попробовал зарепортить ошибки с импортами — а они поправлены в последнем мастере! Отлично, попробуем обновить версию языка.
Пробую собрать из исходников — фигушки, не перепробрасываются cclibdir
.
Пробую собрать из альфа-релиза — фигушки №2, \r
в юникс-скрипте (sic!).
Дальше скрипт становится еще лучше!
./install.sh --url https://github.com/koka-lang/koka/releases/download/v3.1.3-alpha17/koka-v3.1.3-linux-x64.tar.gz
Installing koka v3.1.3 for ubuntu linux-x64
warning: unknown option "--url".
Installing dependencies..
Using generic linux bundle
Downloading: https://github.com/koka-lang/koka/releases/download/v3.1.3-alpha17/koka-v3.1.3-linux-x64.tar.gz
Т.е. жалуемся на неправильный аргумент (который есть в --help
между прочим), а потом его используем :/
Зато после обновления (с 3.1.2 до 3.1.3):
- решилась проблема с
values
(хотя уже не очень-то и нужно было). - внезапно отвалился
string/replace-all
, но простоreplace-all
норм работает. - пофиксился ублюдочный баг с импортами в JS.
- за счет двух моих вмерженных PR можно было убрать костыли.
- даже проблема, из-за которой надо было
rm -rf .koka
делать, стала реже появляться (однако потом снова появилась с большей силой — теперь надо было иногда еще и демон компилятора убивать). - добавили метод
expect
дляmaybe
(правда, имя конченное), удалил свою реализацию вutils
. - смог вернуть
-O3
на фронте.
Bleeding edge разработка, епта!
Потом еще и версия 3.2.0 вышла, пока я сопли жевал со статьей, а потом еще и сразу 3.2.2. Там есть пара интересных фич, но я уже немного устал.
Имя проекта #
Перейдем к самому главному!
Рабочим названием проекта было looKfor
.
Оно было унылым.
Чатгпт предлагал всякую дичь, больше всего понравилось grepka
.
А в итоге в Википедии нашел Klava Koka, до этого про нее вообще не слышал. Жена мне запустила 2 “известные” песни с телефона. Мне показалось, что цитата “Че тебе нужно” из “хита” с чумовым названием “ЛА ЛА ЛА” как раз подходит для проекта. Ну и сферическая баба Клава тоже должна знать, что где лежит.
Муки оптимизации JSON парсера #
Моя реализация была медленной, но она работала. Решил попробовать как-то ее оптимизировать, потому что 10+ секунд — как-то совсем не комильфо.
Первую альтернативу попробовал сделать через нативный вызов JSON.parse
— уж быстрее этого ничего не будет.
Однако поверх него была еще обертка по преобразованию JSON-объекта в JSON-объект Koka — по сути, большой if-else-if с проверкой типов и рекурсивными вызовами с конструкторами оберток.
Пробую запускать… и код прерывается в браузере без ошибки :(
Пробовал и так и сяк — ну не работает, и все!
При этом в консоли все работает нормально.
Я конечно понимаю, что JSON размером 1,6 Мб не так-то просто распарсить, но не настолько же:(
Убийцей оказался опять -O3
.
Попробовал воспроизвести проблему с -O3
в изоляции — получилось плохо.
Методом научного тыка выяснил, что виноват async
— он плохо взаимодействует с рекурсией.
Причем эта падла (эффект async
) проникает в main
, и как-то изолировать десериализацию, чтобы она была вне пространства с этим эффектом, не получилось.
Наконец, в дебаггере докопал до too much recursion
Какого лешего это раньше не всплывало — неясно.
Узнал интересности про рекурсию в браузере.
И понял, почему в консоли работало: там тупо поставлен больше размер стека вызовов: node --stack-size=100000
.
Что делать с этим — не очень ясно.
Хотел попробовать использовать sslice
(read-only view поверх строки) в первой версии парсера — стало уже не ФП, и все равно медленно.
Еще и оказалось, что в мутабельную переменную нельзя присваивать результат match
— только “простое выражение”, чтобы это ни значило.
Решилось промежуточной переменной, но скорости все это не прибавило.
Еще раз попробовал переписать парсер, теперь уже в мутабельном стиле с sslice
и рекурсией.
Открыл наконец, как можно сделать паттерн-гарды, однако счастье было недолгим:
because : guard expressions must be total
т.е. от мубального зависеть там нельзя. Но в итоге и эта реализация тоже оказалась медленной. Даже в консоли это было медленное говно, медленее любой из предыдущих реализаций.
Пробовал ускорить этот медленный вариант через оптимизацию вставки в списки (вставлять в голову, а не в конец) — стало быстрее, но не существенно. Никогда бы не подумал, что буду думать об этом в нормальном коде за пределами абстрактных лаб.
Попробовал vector-list
из библиотеки сообщества — хуже моей обертки сработал.
Даже словил
mimalloc: error: buffer overflow in heap block 0x020080000580 of size 65528: write after 65528 bytes
Вернулся к первой попытке, ужасному ФП коду, впендюрил туда свой тупейший хак для списка — и это наконец заработало! Получается, как будто зря писал еще две с половиной реализации. Но благодаря этому узнал что-то новое и смог существенно упростить оригинальную реализацию.
В продакшен #
Попробовал сначала встроить на сайт через iframe
— господи иисусе, это такой ящик пандоры с разнообразными проблемами, что мама не горюй.
Очень быстро от этого отказался, обошел фиксом, что не тело документа меняется, а выделенный контейнер.
Ну и файлик на сайте, который создает контейнер и грузит основной модуль, все-таки пришлось сделать.
GitHub Action для сборки намястрячил примерно так:
На стадии экспериментов выяснил, что индексер работает медленнее при -O3
, но это некритично, 20 секунд индексации сайта на холодном старте меня устраивают.
Итого #
В итоге эта лабуда работает!
Это было достаточно больно, но все-таки интересно. Фишку с эффектами таки удалось оценить. Какой-никакой вклад в опенсорс добавил.
Наконец-то, написал свои поиск и JSON-парсер! Еще в изначальном плане был стеммер, это проще чем думается, но решил все-таки отложить на попозже. А то так можно дойти до идей с поиском по префиксу, триграммам и хаком для неправильной раскладки…