Переходим на субстанции потяжелее — 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, много параллелей с ним. Крутые фишки так и не смог полноценно оценить.
Про телеметрию
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. Игрулей я доволен, и особенно я доволен тем, что очень быстро получилось что-то сделать и поиграть.
Ограничения и предохранители
Зацепились языками с одним из коллег на тему удобства языка для разработки без IDE. Он заявил, что код на Kotlin невозможно читать без IDE. Вполне органично дискуссия перешла в плоскость того, полезны ли ограничения языка или нет.
Началось все с тезиса про то, что структура файлов в Java (1 файл на 1 класс, 1 папка часть пакета) это полезно и позволяет находить нужный класс быстрее. А вот в Kotlin можно как угодно делать. Поэтому ограничения — это хорошо, они приводят к порядку и простоте. В качестве дополнительных примеров были модификаторы видимости (C против Java), владение (C vs Rust) и т.п. Да и вообще, ограничения формируют/способствуют культуре разработки.
Когда я пытался аргументировать, что ограничение на структуру файлов — весьма дурацкое и особо не помогает, мой оппонент подумал, что я его троллю:) Так-то класс может быть в разных модулях, может быть вложенным и т.п., т.е. это не просто “иди по этому пути”, и хоть какой-то поиск придется рано или поздно использовать.
Весь спор пересказывать не буду, а перейду сразу к выводам.
Я понял, что есть 2 принципиально разных аспекта в языке: ограничения и предохранители. Предохранители — это обычно просто замечательно, они позволяют избегать тупых ошибок. Например, явная проверка на null в Kotlin или владение памятью в Rust — это предохранители. Ограничения — это противоположность выразительности, т.е. что-то, что мешает сделать программисту то, что он хочет. “Я ограничен технологиями своего времени”.
Соответственно, ограничение и предохранитель легко спутать друг с другом (или дебатировать, что хорошо и что плохо). И вот тут-то и зарыт корень наших разногласий: мой коллега искренне считал, что структура папок в Java — это предохранитель и это хорошо, а для меня это еще одно ограничение. В этом плане мне гораздо важнее разложить код по модулям, по ролям и/или по шаблону какой-нибудь трехбуквенной архитектуры. Обычно языку на это плевать, но в Java будет привет от com.company.name.package.folder
.
Мне нравятся выразительные языки. На мой взгляд, хреново и непонятно можно написать на любом языке. Да, в более выразительном языке возможностей может быть больше, но человеческая тупость вообще бесконечна. В свою очередь, выразительный язык предоставляет больше возможностей написать код хорошо. А предохранители должны этому способствовать.
И разумеется, опыт в более выразительных языках влияет на то, как разработчик пишет код. Однако говорить о том, что язык формирует культуру разработки — очень странно. Это как сказать, что ножницы формируют культуру парикмахерской, или молоток формирует культуру стройки. Если у вас инструмент формирует процессы и/или подходы — у меня для вас плохие новости…