Мышки плакали и кололись, но продолжали есть 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-парсер! Еще в изначальном плане был стеммер, это проще чем думается, но решил все-таки отложить на попозже. А то так можно дойти до идей с поиском по префиксу, триграммам и хаком для неправильной раскладки…
Переходим на субстанции потяжелее — Koka
Предыстория
Где-то в конце октября захотелось мне добавить на сайт поиск, даже для этого на GitHub Actions перешел. Плагином пользоваться не хотел, да и как-то скучно это. Тем более что встроенный плагин по сути выплевывает огромную JSON-нину, по которой и идет поиск — звучало это не очень оптимально.
Думал посмотреть на что-нибудь типа SQLite, запущенным в браузере и использовать его полнотекстовый поиск, но там как будто задолбаешься кастомизировать. Вылезло (сам себе придумал) требование, что во время индексации при сборке сайта и при собственно поиске должен использоваться один и тот же морфемный анализ. Потом уже настолько сильно погрузился в тему во время исследования, что захотелось написать что-то самому.
Было еще интересно что-то попутно поизучать (вспомнил свой опыт с Elm). С учетом требования про один язык на стадии индексации и при поиске, я искал что-то новое и с мульти-таргетом и наткнулся на язык Koka.
Впечатление о языке
Документация и инструменты
Описания языка в докладах (1, 2) звучит весьма интригующе: тут тебе и алгебраические эффекты, и широкие возможности с точки зрения комбинаций исключений и типов, и парадигма FBIP: Functional but In-Place (джва года все ждали такое), и приятности типа dot-apply (когда x.func(1,2)
эквивалентно func(x,1,2)
).
Плюс язык может быть транслирован в Си, JavaScript или C# (ну, создатель в Microsoft работает).
То, что язык непопулярный, можно понять хотя бы по тому, что расширение не скопировано в неофициальный маркет VSCode (поэтому был и пост про это). Другой индикатор молодости/непопулярности языка — чат-боты на любой вопрос по нему выдают полную хрень. И подсветки на гитхабе нет.
Справедливости ради, ботов можно понять: документация у языка не супер.
Документация стандартных библиотек, на которую сразу с сайта и не выйдешь, чуть более полезна, но тоже не восхитительна.
На обоих ресурсах нет поиска (ироничненько).
В итоге самым адекватным оказался поиск по исходникам на гитхабе.
Но даже там было не то чтобы много результатов.
Отмечу, что все это надо было еще откопать, а я всего-то хотел узнать, как получить аргументы командной строки — и еле нашел ответ в примере про КЧД.
Найти fst
и snd
для кортежа было сущим кошмаром из-за интересного выбора названий.
Добавляет боли и то, что в расширении для VSCode толком нет автодополнения. И автоформатирования. Спасибо, что хотя бы за счет языкового сервера ошибки некоторые подсвечиваются.
Основы
В целом, жить можно, но проблем хватает.
Вы думали, я шутил про поиск в исходниках вместо документации?
Так вот, чтобы откопать ++
для конкатенации — тоже пришлось попотеть.
Наверняка знающие люди насуют мне, что надо знать Haskell/ML-подобные языки и там это естественно, но епрст, я не помню наизусть этот синтаксис:(
Глобальные импорты — вроде удобно, но до первой неоднозначности. Не очень понятно, что происходит с функциям у которых одинаковое имя, но при этом даже в стандартной библиотеке есть:
fun replace-all( s : string, pattern : string, repl : string ) : string
fun string/replace-all( s : string, regex : regex, repl : string, atmost : ? int ) : string
fun replace-all( s : string, r : regex, repl : (list<sslice>) -> e string, atmost : ? int ) : e string
Отдельного упоминания стоит их реализация: посмотрите, как красиво:
// Replace every occurrence of `pattern` to `repl` in a string.
pub inline extern replace-all( s : string, pattern : string, repl : string ) : string
c "kk_string_replace_all"
cs inline "(#1).Replace(#2,#3)"
js inline "(#1).replace(new RegExp((#2).replace(/[\\\\\\$\\^*+\\-{}?().]/g,'\\\\$&'),'g'),#3)"
// Count occurrences of `pattern` in a string.
pub inline extern stringpat/count( s : string, pattern : string ) : int
c "kk_string_count_pattern"
cs inline "Primitive.Count(#1,#2)"
js inline "((#2) ? ((#1).match(new RegExp((#2).replace(/[\\\\\\$\\^*+\\-{}?().]/g,'\\\\$&'),'g'))||[]).length : 0)"
Определять show
для типов — правильно, но хотелось бы из коробки что-то для DTO
.
Ленивости не хватает: например, нет ничего с ленивым выкидыванием исключения. Да и вообще, ленивость как идею особо не наблюдал ни в документации, ни в коде.
Немного странно, что эффект pure
“грязнее” просто функции без эффектов, т.к. у него есть эффект div
.
Понятно, что это вроде как должно быть удобно для всякой арифметики, но:
При разработке на Elm я жаловался на отсутствие хэш-таблицы из коробки. Посмотрите, какая восхитительная реализация ассоциативного массива у Koka:
/* Map
Todo.
*/
module std/data/map
type map<k,a>
Это она целиком, если что. И да, в примерах есть реализация КЧД, но в стандартной библиотеке ничего нет.
Ладно-ладно, я ее позже все-таки нашел: есть экспериментальное ответвление стандартной библиотеки, которое когда-нибудь через никогда может быть будет вмержено в основной репозиторий языка.
Управление зависимостями
Пришло время рассказать про менеджер пакетов. Тут все как в меме:
Т.е. его нет. А как какать? Официальный ответ — использовать гит модули (sic!).
Хотел через гит-модули подключить, но там нельзя только подпапку выдрать, а шелл-скриптом — некрасиво… в итоге из-за сложности принятия решения тупо забил на этот проект аж на месяц.
Потом наконец-то вернулся и решил, что пошло оно все нафиг, и будет скрипт “менеджер пакетов”, который клонирует библиотеку(и) в ./libs
При попытке использования альтернативной библиотеки возникла проблема: внутри нее есть ссылки на std
.
koka -i./libs my-source.kk
— вот так вроде правильно, но альтернативная std
будет после стандартной.
В итоге мой говноскрипт тупо копирует альтернативную стандартную библиотеку в ./std
.
И при таком расположении используется именно она.
Отладка и дебаг
Ошибки компилятора, мягко говоря, не очень понятные:
uncaught exception: unexpected Nothing in std/core/maybe/unjust
Это, если что, вся ошибка.
Впрочем, сам виноват: нечего было unjust
использовать.
Пришлось попыхтеть, чтобы понять, что в этой функцию поиска по ключу надо было добавить… доказательство что есть ==
!
fun get(maplike: list<(k, v)>, key: k, ?(==) : (k,k) -> bool)
(этот код я удалил, когда в экспериментальной стандартной библиотеке нашел hashmap
).
Но даже с hashmap
я все равно задолбался подбирать правильные функции, которые надо добавить в область видимости.
В итоге создал тикет, и что круто — почти сразу ответили, баг поправили и я смог кодить дальше.
Дебаггера нет, поэтому по первости использовал println
.
Но есть нюанс: это же эффект!
Поэтому надо добавить эффект консоли по всей цепочке вызовов.
Правильно, но неудобно.
С другой стороны — если прям обрабатывать все ошибки, то сузить проблему можно довольно быстро.
Чуть позже обнаружил, что есть trace("ololo")
, который не добавляет эффект, но выводит в консоль.
И там уже появляется … unsafe-total
— по сути, бэкдор для системы эффектов.
Поддержка Unicode
Довольно быстро обнаружил, что JavaScript не поддерживает юникод с точки зрения \b
и \w
в регулярках (место для вашей rage-картинки про JS).
Соответственно, Koka, который транслируется в JS, имеет те же проблемы.
А вот с собственно символами все еще хуже. Я бы даже сказал, что поддержка чего-то, что не ASCII отсутствует, несмотря на какие-то ошметки кода в стандартной библиотеке.
Думал отказаться от Си-таргета…. чтобы обнаружить, что в JS-таргете не работают хэшмапы из‑за отсутствия сида. Но это хотя бы можно поправить костылями — например, использовать КЧД. А вот с файлами теперь работать не получится, потому что откуда эти API в браузерном JS?
Это открытие меня весьма расстроило, и проект опять был надолго поставлен на паузу. С учетом того, как быстро ответили на первый тикет, было печально не увидеть никакой реакции на второй. Я даже открыл PR, чтобы добавить базовую поддержку Unicode в Cи-таргет, но тоже не получил ни ответа, ни привета. При этом не могу сказать, что я был в восторге от процесса разработки — одно только тестирование путем сравнения вывода с текстовым файлом о многом говорит.
В общем, на этой стадии я решил, что потраченного времени жаль, и мужественно решил забить на все это окончательно.
Итого
В целом, опыт был интересный. Немного жаль, что в итоге никаких толковых артефактов не получилось. У языка есть потенциал, и идеи интересные, но все слишком сыро и недружелюбно. ПСТР от Elm, много параллелей с ним. Крутые фишки так и не смог полноценно оценить.
UPD: и через какое-то время я собрался с силами и продолжил.
Про телеметрию
TLDR: следить за пользователями — это обычно плохая идея.
Вроде бы это должно быть очевидно (с точки зрения пользователя), но у всяких там продактов иногда возникает соблазн “собрать немного данных”. Поэтому некоторое время назад я решил поисследовать эту тему поглубже, чтобы иметь более аргументированную позицию.
Источники
Есть отличная серия постов от разработчика Go, в которой разобраны плюсы телеметрии, аргументы за нее и обозначены лучшие практики по ее внедрению. Вкратце:
- Баг-репорты содержат недостаточно информации.
- Опросы пользователей содержат недостаточно информации и нерепрезентативны.
- Многие негативно относятся к телеметрии из-за в основном предубеждений и стереотипного мышления (“они собирают каждый наш клик!”), но в некоторых случаях это вовсе не преувеличение.
- Без статистики использования непонятно, какие функции используются/нужны, а какие нет, и время разработчиков может быть потрачено впустую.
- Пользователи не знают, как должно вести себя ПО, поэтому не дают достаточно хорошие баг-репорты, а некоторые проблемы могут остаться незамеченными несколько релизов.
- Телеметрия должна быть прозрачной: надо собирать как можно меньше данных, показывать пользователю, что собирается, не содержать идентификаторов, ее должно быть легко отключить, данные должны быть текстовыми.
- Решение о внедрении телеметрии должно быть публичным.
- Собирать со всех все не нужно, достаточно сравнительно небольшой выборки для репрезентативных данных.
- С каждой установки данные собираются не постоянно, а изредка.
- Все собранные данные должны быть публичны.
- Телеметрия должна быть включена по умолчанию, но ее отключение должно быть простым.
Последний пункт был отредактирован, потому что после обратной связи от сообщества разработчики решили отключить телеметрию по умолчанию, т.е. пользователь должен осознанно ее включить. Вообще, opt-in (телеметрия по умолчанию выключена) против opt-out (включена по умолчанию) — один из ключевых вопросов в этой теме. В этой же серии постов можно прочитать про это подробнее: тяжелее получить данные, надо пиарить подключение, согласившимся нужно будет чаще посылать данные, будет перекос в собранных данных и т.п.
А есть мнение, что замена на opt-in сделает телеметрию бесполезной. Т.е. по сути opt-out — плохо для пользователей, а opt-in — бесполезно для разработчиков. Но в той же статье есть и аргументы за телеметрию, причем в оригинальном видении из цикла статей. При этом отмечается, что не последнюю роль в негативной реакции сообщества сыграло то, что автор предложения работает в Гугле (а он уже давно не “don’t be evil”).
Разработчики Audacity хотели внедрить телеметрию, но в итоге после негативной реакции пользователей пришли к более скромному варианту, который покрывает только вопрос более подробных баг-репортов.
Некоторые аргументируют, что телеметрия бывает разной, и не вся телеметрия — это слежка, а стоит думать о ней как о голосовании, мол, ваш “голос” будет учтен. И вообще, open source разработчикам тяжелее получать данные о пользователях, чем разработчикам проприетарного ПО.
Если почитать статьи про телеметрию с более пользовательской точки зрения, то там будут в основном негативные или язвительные комментарии. Примеры:
- Telemetry is definitely your enemy
- The Telemetry Fallacy
- Fedora Project mulls ‘privacy preserving’ usage telemetry
Отдельно можно отметить комментарии на HackerNews, тысячи их:
Там есть мнения как с точки зрения пользователя, так и с точки зрения разработчика.
Я постарался как-то это все агрегировать ниже.
Аргументы “за”
Вкратце: Больше данных хорошо, проще получать инсайты, проще приоритизировать задачи и развивать продукт.
- Без телеметрии невозможно собрать статистику об использовании продукта.
- Статистика использования позволяет приоритизировать разработку функциональности.
- Статистика позволяет удалить ненужную функциональность без негативных последствий.
- Люди лгут, телеметрия — нет. Опросы пользователей содержат недостаточно информации и нерепрезентативны.
- Телеметрия позволяет обнаруживать проблемы, о которых люди и не задумывались.
- С телеметрией предоставляется больше данных чем в обычных баг-репортах.
- Баг-репорты требуют усилий и не все пользователи их отправляют/могут отправить, телеметрия экономит усилия пользователя по отправке данных.
- Телеметрия позволяет делать более надежные продуктовые решения. Без данных принимаются плохие решения.
- Телеметрия — это способ “голосования” людей за улучшения в продукте.
- Обратную связь разработчики получают только от самых “говорливых” пользователей, а с телеметрией — от всех.
- Понять сценария использования системы и интегрировать эту информацию в процесс разработки и тестирования весьма тяжело без метрик.
- Тяжело тестировать оптимизации ПО без метрик с пользовательских машин, держать парк своих серверов для тестирования производительности — дорого.
- Телеметрия может помочь создать более прибыльный продукт.
- Все веб сервисы и так собирают данные при каждом обращении к сервису, чем телеметрия хуже?
- Большинству пофигу на “приватность”.
Аргументы “против”
Вкратце: данных и так полно, качество данных от телеметрии сомнительно по многим причинам и не даст всей картины, на основе телеметрии можно сделать неверные выводы, надо думать о ИБ.
- У вас и так полно данных о платформах, которые вы получаете от статистики установок, User-agent и т.п. Интернет так работает и всех это устраивает, т.к. большинство понимает, что посылается и когда. Активная отсылка данных с клиента — плохо, потому что это вторжение на пользовательский клиент, которое пересекает границу, созданную браузером/протоколами.
- Обложите бекэнд трейсингом, и получите дофига данных.
- Если у вас не веб-приложение, то вместо телеметрии можно добавить функциональность по отправке ошибок.
- Если у вас не веб-приложение, то возможно ваши пользователи достаточно умны, чтобы написать нормальный баг-репорт.
- Если вы не знаете, как работает ваш продукт или что нужно пользователям, то телеметрия не решит проблемы вашей разработки. Наймите нормального продакта и нормально делайте исследование своих пользователей, прежде чем внедрять телеметрию. Желание внедрить телеметрию может быть сигналом о том, что продукт стал таким сложным и/или хрупким, что пользователи не напрягаются давать обратную связь. Или отправить баг так сложно, что никто этим не заморачивается.
- Просто данных недостаточно для того, чтобы делать какие-то выводы. Нужно строить гипотезы и подтверждать их, иначе это будут просто догадки. Легко “переоптимизировать”. Любая метрика перестает быть хорошей когда она становится самоцелью. Для оценки качества есть соблазн использовать эту же метрику. Телеметрия не покажет ни намерений, ни эмоций пользователя.
- Если фича редко используется, это не значит, что ее можно удалить (аргумент про огнетушитель и бэкапы).
- Телеметрия может быть формой дискриминации: “только 5% пользователей пользуется фичей, давайте ее удалим” или “если ты не шлешь телеметрию, то твое мнение не учитывается”.
- Разработчики опенсорса не должны заботиться о том, как используют их бесплатное ПО пользователями, которые ничего не дают им взамен. А ответственные пользователи помогают проектам, открывая пулл-реквесты и баг-репорты.
- Телеметрию нельзя использовать на удаленной фиче или на желаемой фиче, для этого надо использовать другие источники информации и знать своего пользователя.
- С телеметрией легко скатиться к “среднему пользователю” и “среднему по больнице”, что обычно плохо.
- Для новой фичи данные будут искажены из-за ранних пользователей и не дадут хорошей картины.
- У вас и без данных от телеметрии полно работы (кто-нибудь работал в успешном продукте, где не было большого бэклога?). Если у вас есть четкое видение продукта и вы нормально исследуете пользователей, то у вас достаточно и работы и приоритетов.
- Многие продукты больших компаний (Google, Facebook и т.п.) обложены телеметрией — становятся ли они со временем лучше? Точно ли телеметрия дает выгоду пользователю?
- Анонимизация снижает качество собираемых данных, но при этом способы деанонимизации совершенствуются. Т.е. если данные нормально анонимизировать, то они станут бесполезны, а если это не делать — то это нарушит приватность пользователей. Некоторые еще переживают, что с ИИ анонимность вообще недостижима, а пользователей с уникальными (странными) окружениями легко сдеанонить фингерпринтингом.
- Если продукт с открытым исходным кодом, то легко его модифицировать, чтобы посылать мусор вместо данных.
- Компании плохо заботятся о ИБ, данные пользователей рано или поздно утекут. Даже если собираемые данные надежно защищены, не факт, что это не поменяется в будущем или что они не будут слиты по первому запросу госорганам.
- Opt-out — плохо для пользователей, а opt-in — бесполезно для разработчиков.
- Opt-out — в серой зоне с точки зрения легальности (с точки зрения GDPR и конвенций ООН).
- Телеметрия выгодна только разработчикам и нужна, чтобы сократить расходы.
- Телеметрия может быть первым шагом к агрессивной монетизации и сместит фокус разработчиков на платных пользователей. Будут внедрятся только те фичи, которые прибыльны, а не те, которые полезны пользователям.
- Телеметрия должна быть прозрачной, но публичные данные о статистике пользователей могут быть использованы против вашей компании.
- Если продукт был успешен X лет без телеметрии, то действительно ли она необходима? Как-то же справлялись разрабы раньше без нее.
- Всегда будут люди, которые откажутся от телеметрии, и это чаще продвинутые пользователи. А еще телеметрию может быть заблокирована блокировщиком рекламы, если у вас веб-приложение.
- Телеметрия — потенциальная брешь в системе, как со стороны клиента, так и сервера, и увеличивает поверхность атаки.
- Телеметрия — это доп. расход ресурсов, как пользователя, так и компании. Если все программы будут собирать телеметрию, приведет ли это к чему-то хорошему? Чем ваше приложение такое особенное, чтобы делать это?
- Если решение принимается децентрализовано, сообществом, то будет достаточно мнений, чтобы обойтись без телеметрии.
Итого
В основном телеметрию хотят разработчики, а пользователи (которые высказывают свое мнение) в целом против нее.
Большинству пользователей на все выше скорее всего насрать. Приложения на телефоне и какой-нибудь поиск гугла или хром собирают о вас столько данных, что вас можно будет заменить роботом и никто не заметит. Ну и, увы, как-то странно верить, что в мире осталась какая-то приватность, когда каждое устройство и программа что-нибудь собирают, улицы обвешаны камерами, а товар в бесплатных сервисах — это вы.
Доверие очень тяжело заслужить и легко потерять (“и невозможно забыть”:) ). Если прям надо-надо телеметрию, то лучше быть максимально прозрачными и следовать советам от разработчиков Go, а насчет opt-in или opt-out — думать, что лучше подходит. Но в первую очередь стоит подумать, а точно ли она нужна вообще (скорее всего нет). С ее помощью можно получить какие-то данные, но у них может быть сомнительное качество.
The more these things are happening and the more I read about it, the more I understand and agree with Richard Stallman.
Автоматический рефакторинг кода с помощью OpenRewrite
Попробовал применить OpenRewrite к репозиторию Gradle и если вкратце, то все очень плохо.
В официальной документации очень солидные заявления — утилита позволяет делать автоматический рефакторинг на основе рецептов, при этом код преобразуется в семантические деревья, к ним применяются трансформации и результат “минимально инвазивно” записывается обратно. Основная фишка инструмента, как я понял, заключается в том, что вы пишете правила для рефакторинга (или используете готовые), а потом применяете скопом к своей большой кодовой базе (возможно из нескольких репозиториев), экономя время на ручной труд.
Но реальность полна разочарований.
Подключение к проекту и запуск
Довольно быстро обнаружилось, что плагин для Gradle не поддерживает современные фишки: кэш настроек, параллельное выполнение (с весьма странным багом) и т.п.
Вдобавок к этому, парсер давился на Groovy файлах (у нас большинство тестов написаны на нем), что довольно странно, ведь вроде инструмент поддерживает Groovy.
Но это еще цветочки, эта приблуда еще пыталась обработать jsp
из ресурсов!
Т.е. она даже не понимает, где исходники и на каком языке, и видимо молотит все текстовые файлы подряд.
Через час (sic!) после запуска я получил… OOM. Возможно где-то есть утечка памяти, либо в самом Gradle, либо в плагине. Я в итоге плюнул и написал скрипт, который применяет утилиту к одному подпроекту за раз. Память пришлось поднять только для одного большого подпроекта.
Работает утилита, мягко говоря, не быстро: чтобы весь код прошерстить, ей понадобилось больше 2 часов (и это на топовом маке). Впрочем, насчет производительности разработчики вряд ли парятся — платный вариант как раз обещает ускорение за счет своей БД, чтобы не перестраивать семантические деревья каждый раз с нуля.
Сами изменения
Сначала я попробовал применить рецепт по миграции на Java 8 — первая версия Gradle была написана аж 2008, кода довольно много, и что-то могло остаться в “старом стиле”. Но этот рецепт никаких изменений не внес.
Далее я попробовал миграцию на Java 17 — там уже была куча правок, но не стал углубляться, т.к. ее обновление пока только запланировано.
Пока я ждал первого прогона, почитал документацию в целом и поискал, какие есть интересные готовые рецепты.
Инструмент очень сильно сфокусирован на Java (даже не JVM).
Кроме обновления версии Java я нашел только 2 интересных рецепта: CodeCleanup
и CommonStaticAnalysis
.
Там были и другие, например для обновления билд-скриптов, миграции на новую версию Hibernate и т.п., но большинство из них были довольно узко применимыми.
Сами изменения были не без ошибок:
- добавлялся
final
к свойствам, которые потом изменялись, и к классам, у которых были наследники; - удалялись/добавлялись параметры типов там где не надо;
- была добавлена куча пустых конструкторов (видимо, чтобы сузить область видимости), но ценности я в этом не увидел;
- пока не добавил исключения, Groovy файлы были обновлены по непонятным правилам: например,
as SomeClass
превратилось вasSomeClass
.
Отдельно отмечу упрощение условий: оно было настолько тупое, что заменило
!(methodName != null ? !methodName.equals(that.methodName) : that.methodName != null);
на
methodName == null ? !methodName.equals(that.methodName) : that.methodName != null;
что буквально вызовет NPE.
Тут у меня начались вьетнамские флешбеки от кривых #define
в Си, где надо было огораживать все скобками, иначе получится шляпа.
При этом IntelliJ через пару Alt+Enter заменит этот код на человеческий
Objects.equals(methodName, that.methodName)
Другой показательный пример:
public boolean isEmpty() {
return size() == 0;
}
был заменен на
public boolean isEmpty() {
return isEmpty();
}
Да-да, это классическая реализация StackOverflowError
.
Все вышеперечисленное приводило к ошибкам компиляции и к ошибкам от существующих средств проверки качества кода (-Werror
, ErrorProne, CodeNarc и т.п.).
Из-за этого я переписал скрипт, чтобы после применения рецептов к подпроекту запускалась компиляция.
Разумеется, это еще больше замедлило и без того небыстрый процесс.
И ошибки компиляции править приходилось вручную, так что продуктивность полетела прямиком в помойку.
Самое отстойное, что некоторые рецепты проще всего было бы отключить совсем, но такой опции у инструмента нет. *звуки грустного тромбона*
Вывод
Я был довольно упертым и все-таки применил рецепты ко всем проектам. В итоге было обновлено 1700+ файлов. Было ли там что-то такое, ради чего стоило проходить через эти пляски? Если кратко, то нет.
Были изменения в форматировании (местами сомнительные, типа Йода-условий или порядка модификаторов), манипуляции с импортами, упрощение условий (см. пример выше), везде была добавлена куча final
(даже там где не надо), лямбды были заменены на ссылки на методы, убраны ненужные параметры типов, мелкие изменения типа замены .size == 0
на .isEmpty()
или страшной восьмеричной системы с 0777
на понятную десятеричную с 511
.
Если ваша кодовая база маленькая, то вы вряд ли будете будете писать новые рецепты — проще руками через IDE все мигрировать. Если кодовая база большая, и есть платформенная команда, то скорее всего код и так уже будет довольно однородный и кучу вещей можно будет решить скриптом/простыми текстовыми преобразованиями. Кажется, рецепты имеет смысл писать только для чего-то специфичного. Самая потенциально полезная часть инструмента — общеприменимые рецепты (типа миграции Java), но их не очень много и покрывают они на удивление мало.
Но все это просто уничтожается качеством инструмента.
От чего-то, основанного на семантическом представлении, которое строится тыщу лет, я ожидаю, что итоговый код будет хотя бы компилироваться и не содержать очевидных ошибок.
А по факту все это не сильно лучше текстовых замен или #define
в Си со всеми их недостатками.
IntelliJ гораздо лучше понимает код и его контекст.
И такое ощущение, что с ней сделать те же операции было бы быстрее.
В общем, great idea, does not work.
P.S. Затеял я это все для того, чтобы посмотреть, какие есть инструменты для миграции кодовой базы с одного языка на другой. ChatGPT и прочие AI штуки сразу отмел, т.к. хотя с маленькими кусочками он и хорошо справлялся, но я думал, что недостаточно надежно будет его применять для большого количества кода. Но кажется для этой задачи он явно лучше этого изделия. Еще находил Txl, но он показался слишком сложным. Планирую попробовать его в будущем.
Сапер для вымышленной консоли на Nim
На выходных запилил игрулю, вариацию Сапёра.
Поиграть можно тут (с компа стрелочками удобнее, но на мобилах тоже работает), а исходники почитать — тут.
Про саму игру
Идея пришла в голову не слишком случайно. Недавно поставил себе классического сапера на планшет и залипал в него. Но с “аккордеонами” (которые рекурсивно открывают соседние ячейки если все однозначно) игра превращается в тупейший кликер на скорость. Прям думать-думать надо раза 2-3 за игру на “Профессионале”, и обычно после этого приходится делать случайный выбор.
“Ездить” по полю было намеренным решением. Кликать скучно, а так себя можно представить роботом, который уволился из доставки и пошел в саперы. В голове у меня застряла прочная ассоциация с Bomberman, в который тоже недавно играл, — там тоже кто-то ходит и связано со взрывами, но общего тут только ходьба по большому счету.
Получившаяся игра чисто случайно (потому что консоль 160x160 пикселей) получилась с такими же параметрами как у “Любителя”, однако она тяжелее. Тут надо думать чаще, да еще и помнить, где мины стоят (отсутствие флажков тоже было намеренным). И следить за тем, чтобы случайно не уехать дальше, чем нужно: чаще всего я проигрываю как раз из-за этого.
Вообще я заметил, что и в обычной жизни некоторые вещи хочу делать побыстрее и без особых раздумий. В результате делаю тупейшие ошибки и выходит дольше и хуже:) Все как в поговорке. Возможно, эта игра потренирует мою усидчивость.
Консоль и ее ограничения
На платформу я наткнулся, когда что-то искал для Zig в рамках своего предыдущего приключения:
там есть еще и другие языки, чтобы на них пострадать
В целом, программировать под нее оказалось довольно просто. При этом она не идеальна (я нашел пару багов, придумал фичу и даже сделал мини-пуллреквест), но компенсирует это “ламповостью”.
И наличие ограничений тут сыграло на руку: я очень быстро выкинул несколько идей (некоторые, правда были неплохие), которые могли бы затянуть разработку на недельку, и смог быстро получить что-то рабочее.
Nim
Nim я выбрал по принципу “что-то новое, но при этом хотя бы слышал” (оказывается, для этого баззворд даже есть). Про него до этого я знал только, что да, вот есть такой язык, похож на питон чем-то, но при этом системный, и вроде хвалят.
Писать на Nim было легко. Я опять поленился и не почитал даже простейшие туториалы (1, 2), но первую играбельную версию написал без проблем, просто используя те конструкции, которые казались логичными для питона/сей и при взгляде на код рядышком из шаблона. Углубиться в язык понадобилось только когда проверку решаемости добавлял.
С точки зрения синтаксиса — да, напоминает питон с его значимыми пробелами и некоторыми особенностями синтаксиса, и еще в чем-то есть сходство с F#.
Есть непривычные вещи, например, конкатенация строк через &
вместо +
или перевод в строку с помощью доллара ($someInt
).
Забавно, что лямбды — это сахар, и для x => x + 1
надо буквально импортировать std/sugar
.
Немного расстроили структуры, там обязательно надо указывать имена полей при создании, Cell(x: p.x + 1, y: y)
мне не очень понравилось и я в итоге оставил просто две переменных во многих местах.
Switch не такой мощный, как хотелось бы (даже Kotlin’овский when
поинтереснее), но это я по скаловскому скучаю.
Впрочем, это все скорее вкусовщина.
Одна из крутых фич — все методы это по сути расширения.
Т.е. x.pos(y)
это сахар для pos(x, y)
.
С точки зрения программиста на Cях со структурами, да и с точки зрения ФП-шника с ворохом DTO — очень удобно.
Еще крутая фишка — оптимизированные множества.
Стандартный set
— это битовый массив.
Итераторы с yield
— мое почтение, тоже очень удобно.
Есть и всякие плюшки для времени компиляции, например when
или static: assert
, но чтобы их по настоящему оценить, нужно видимо что-то мультиплатформенное делать.
Не понравилось, что объявление функции должно быть обязательно раньше его использования: не очень дружелюбно к косвенной рекурсии, да и организовать код в файле чуть-чуть тяжелее. Возможно, надо было разбивать код на модули, но я решил, что оно того не особо стоит: весь код занимает меньше 300 строк.
Прям неожиданно споткнулся об интервалы: они паскалевские/котлиновские, т.е. вместо привычного for i in 0..n
надо писать for i in 0..<n
.
С одной стороны, более явно, чем “в устоявшемся” стиле, с другой — тяжело обнаружить классическую ошибку на единицу.
В этом плане отстойно, что не было никакой проверки на выход за границы массива.
И еще один бесючий момент — жесткая сегрегация численных типов без неявной конверсии между ними, т.е. int
, uint8
, uint32
и т.п.
Подобное было уже с Rust и Zig, и вроде в Nim чуть лучше, но все равно не нравится.
Справедливости ради, один потенциальный баг может быть за счет этого был обнаружен.
С точки зрения инструментов, видимо, это модный тренд: делать менеджер пакетов, совмещенный с инструментом сборки. Да еще чтобы скрипты сборки были на этом языке (хотя по факту это мало отличается от Make с набором альясов для баш-команд).
В целом в Nim есть интересные фичи (но и сомнительные тоже), и ощущения после него остались приятные.
Разработка
На все-про-все у меня ушло часов 10-11, и большую часть времени я потратил на установку рабочего окружения. Причем дважды: локально, чтобы не curl-sh (сесурити!) и в GitHub Actions (благословлен пусть будет act, без него было бы еще дольше).
Первую играбельную версию я получил всего через 2-3 часа после настройки окружения.
Потерял немного времени на диагностику проблемы с рандомом, чтобы в итоге использовать трюк со счетчиком фреймов.
Обнаружил еще, что функция обновления фрейма не блокирует последующий вызов, но это тоже стоило времени — для меня блокировка была как что-то само собой разумеющееся. А неправильная обработка этой ситуации вызывала проблемы с обработкой нажатий клавиш.
Наконец, подобрать более-менее пристойную картинку для логотипа тоже заняло время: найти AI-сервис генерации без регистрации и смс, выяснить, что с текстом все еще есть проблемы у старых моделей, а потом еще отмасштабировать картинку и с помощью ImageMagick снизить количество цветов до 4. Ну и поиграться с цветами, разумеется.
Из собственно алгоритмических вещей самой трудозатратной задачей было обеспечение того, что в игру можно выиграть. В ранних версиях этой проверки не было, и была возможна ситуация, когда пустое место в углу было перегорожено двумя минами. Я пытался придумать что-нибудь умное, чтобы влезть в “почти” константную память, но в итоге забил и оставил простой BFS. И была еще пара экспериментов, от которых я отказался.
Был еще забавный момент, когда я тестировал небольшое изменение, связанное с обновлением состояния игры. Я спешил пройти уровень и постоянно делал мелкие ошибки, и в итоге долго не мог отладить поведение просто потому, что не мог выиграть :)
Итого
В целом у меня остались вполне позитивные впечатления от платформы и от Nim. Игрулей я доволен, и особенно я доволен тем, что очень быстро получилось что-то сделать и поиграть.