Я третий месяц работаю на маке и у меня почти не бомбит
На работе выдали мак, описываю свой опыт. У этой заметки нет цели, только путь.
Клавиатура
Одним из первых впечатлений была раскладка клавиатуры, но об этом я уже писал. Дополняют непривычную раскладку непривычные горячие клавиши, которые порой могут состоять из трех или даже четырех нажатий (как вам, например, Shift + Command + 4 + Space для скриншота окна?). Иногда чувствовал, что рука расположена как-то так:
Причем не все можно поменять — например, раскладку клавиатуры на Alt + Shift уже не переключишь. На ноутбучной клавиатуре это еще терпимо (можно нажать Fn), но на внешней — уныло. Бонусом идет тупка с путаньем Ctrl vs Command при переключении на домашний комп. А еще не хватает Home и End — только подпорки, которые не везде работают. Справедливости ради, иногда комбинации логичнее, чем “привычные”: например, нормально работающая вставка в терминале (потому что она висит на Command + V, а управляющие команды — на Ctrl).
Мышь
На тачпаде немного поработал, но все-таки это не мое — мышка удобнее. Долгое зажатие вместо правой кнопки — сомнительное решение. 4-5 кнопки из коробки не работают — только через костыли в Karabiner. Иногда даже при не очень большой нагрузке курсор сильно лагает и “дергается” — это жутко бесит, особенно когда из-за этого промахиваешься по элементам (и это явно не проблема с оборудованием, а с приоритетом обработки ввода). Не помню, чтобы винда или линукс такое себе позволяли.
Надежность
Бытует мнение, что в Apple лучшие инженеры™ создают единственно верную ОС™, которая идеально работает на специально подобранном оборудовании™. Мол, не надо поддерживать зоопарк писюков и все поэтому работает зашибись. Так вот, это брехня собачья.
Я не мог залогиниться в App Store. Казалось бы, критичная функциональность, чтобы ПЛОТИТЬ деньги. Но нет, логинишься, что-то там грузится, потом логинься опять. И я такой не один. Уже не вспомню, как у меня в итоге получилось это сделать, но наверняка “надо было просто держать в правой руке”.
Другой прикол — после обновления (Monterey 12.1) в Safari перестал работать микрофон. Похожая проблема была еще в 2018. Я потратил несколько часов на попытки решения этой проблемы, даже пробовал стремную комбинацию со сбросом памяти Option + Command + P + R, но это никак не лечилось. Поскольку Safari — важный системный компонент (как Internet Explorer), то ни переустановить, ни изменить его версию нельзя, пришлось пересесть на другой браузер, чтобы в Meets говорить. И новое обновление (12.2) эту проблему не решило.
В качестве третьего примера скажу, что у меня один раз завис Dock (панелька внизу для запуска приложений). Благо был открыт браузер и терминал, и я смог его перезапустить, но осадочек все равно остался.
Наконец, в выданном мне стареньком маке уже лагает видеокарта или встроенный монитор (показывает периодически случайный набор пикселей) и вентилятор шумит, как космолет.
Приложения
brew
— просто must have, потому что из коробки почти ничего не стоит. К сожалению, при каждом запуске он качает кучу всего (например, свои обновления). Магазин — это конечно прикольно, но все, что я ставил из него, оказалось бесполезным.
Из коробки нет нормального текстового редактора — только вшивый блокнот, который поддерживает немного форматирования, и приложение для заметок, 1-в-1 как на iOS. После почти любого линукса, в котором будет блокнот с подсветкой кода (gedit, kate и т.п.) — уныло. Для офисных форматов поставил LibreOffice, для всякой мелочи использую что попало.
Нет аналога Paint. Где теперь рисовать убогие мемы? Можно, конечно, использовать встроенный Preview, но он хорош только для мелких правок — типа шлепнуть текст поверх скриншота или обвести что-нибудь (что и редактор в телеге умеет). Добавить пустое место — уже нетривиально. Все редакторы, которые попробовал, либо лагали, либо тупо не запустились, либо имели настолько порезанный функционал, что смысла в них не было. Видимо, настоящие маководы — это дизайнеры и сразу в фотошопе все хреначат.
В мире, где правит SaaS, это все некритично, но впечатления, что получаешь готовую для большинства задач систему из коробки — нет.
Безопасность
Классный подход к безопасности и изоляции приложений — например, можно явно запретить приложению пользоваться камерой, слать уведомления или даже получать доступ к папкам на диске. Но это палка о двух концах — если хочешь пошарить экран в слаке, то изволь запросить сначала разрешения, потом перезапустить слак и перезайти в созвон. Безопасность против удобства, классика:)
Но бывают моменты, когда Большой Брат Стив Знает, Что Тебе Это Не Нужно. Например, в рутовой ФС ничего создавать нельзя. Совсем-совсем, даже если очень надо.
Интеграции
Очень классная интеграция почты и календаря с Гуглом — один раз залогинился, и все работает. Разумеется, если почта не на Гугле — то страдай. Интеграция с Intellij Idea, когда в Dock показывается прогресс билда — просто супер. Увы, еще примеры тяжело назвать.
UX
По идее, я должен был проникнуться и удивляться, как все удобно и продумано. Конечно, есть приятные мелочи и просто забавные штуки (типа анимированных аватаров на логине входа), но есть и мелкие раздражения (например, в файловом менеджере не работает вырезание, а в Safari нет иконок для закладок). Вау-эффекта система в целом не произвела.
Со всякими свистелками и перделками с окошками я наигрался еще в 2007 с Beryl верните мне мой 2007. И мак вроде ими славится, но все равно некоторых вещей не хватает: нельзя, например, закрепить окно поверх остальных (хотя при этом быстрые заметки открываются как раз поверх всех окон).
Чувствуется, что Apple идет на сближение с мобильными интерфейсами — приложения и иконки как в iOS, некоторые действия похожи на мобильные (например, в Safari, внезапно, долгое нажатие левой и нажатие правой кнопкой мыши — это разные действия, и для перехода назад на несколько шагов в истории нужно именно долгое нажатие, а логичная ПКМ — не работает). Понятно, что трендам надо следовать, но не уверен, что это хорошая идея (луддит во мне ворчит).
Ожидаемо расстроила логичность настроек и вообще возможность настройки чего-либо. Например, чтобы включить звук уведомлений в слаке, нужно включить в приложении внутренние звуки через системные настройки, а не разрешить издавать звуки уведомлениям слака в центре уведомлений. С переходом в сон вообще беда. Линусковый ноут я выключал тупо закрыв крышку и выключив мышку. С маком такой номер не пройдет. Ты закрыл крышку, но у тебя подключен внешний монитор? Бро, ты наверно еще работаешь, подумал за тебя, в сон уходить не буду. Ты отсоединил моник, закрыл крышку, но сдвинул мышку на 0.00000001 мм, чтобы ее выключить? Бро, наверно ты хочешь еще поработать, проснусь обратно. В итоге я выключаю мышку, потом тачпадом через меню включаю сон, и только потом закрываю крышку. Спасибо, Apple, что думаете за меня, как мне удобно!
Рестарт для обновления кажется уже таким устаревшим. Не понимаю, как это все выносят, особенно когда это занимает минут 30, в течение которых с ноутом ничего делать нельзя.
Итого
Несмотря на то, что статья получилась в привычном стиле (“У меня бомбит от X”), в целом жить на маке можно. Часть перделок можно перенести на линукс (если захотеть), часть довольно уникальна. Работать определенно удобнее, чем в винде, примерно понятно, почему некоторые разработчики хотят работать на нем. Но линукс лучше (кек), как минимум потому, что при сопоставимом уровне удобства платишь гораздо меньше:).
Мои впечатления от Kotlin-JS
… или мышки плакали, кололись, но продолжали пробовать фреймворки для генерации кода для web-странички.
Зарождение идеи и MVP
Идея ко мне пришла, когда я ждал одобрения своего пулл-реквеста в Kotlin. Заинтересовался вопросом — как долго можно будет ждать ответа, особенно с учетом большого числа открытых PR. Попробовал поискать готовое решение, но ничего не нашел.
Потом потратил примерно полчаса-час на то, чтобы накалякать скрипт на питоне, который подключался к GitHub REST API, скачивал все пулл-реквесты и считал перцентили для времени закрытия пулл-реквестов. Потом решил, что штука получилась полезная, надо сделать доступной для всех. Kotlin Multiplatform прет из всех щелей, Kotlin билдится в JavaScript уже тысячу лет, в Scala переделать JVM в JS было довольно просто — почему бы и нет? Думал, что использую кросс-платформенные библиотеки, а потом сделаю тоненькую прослойку для фронта и будет готово.
Hello world
Выставить экспорт оказалось сложнее, чем в Scala. Во-первых, надо добавлять префикс с именем файла (github_pr_stats.someFunc
вместо просто someFunc
). Во-вторых, если функция лежит в каком-нибудь пакете, то писать надо будет полное имя (github_pr_stats.ru.ov7a.pull_requests.ui.someFunc
). Можно использовать аннотацию @JsName("shortName")
, чтобы писать… github_pr_stats.ru.ov7a.pull_requests.ui.shortName
. В итоге я так и не нашел способа сделать человеческое имя для экспорта. В Scala для этого было достаточно написать @JSExportTopLevel("shortName")
.
Intellij иногда немного тупила и не подсвечивала нормально код, иногда ломалась навигация. Один раз вообще какая-то фигня внезапно случалась с webpack с очень понятной ошибкой
[webpack-cli] Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration has an unknown property '_assetEmittingWrittenFiles'. These properties are valid:
object { bonjour?, client?, compress?, dev?, firewall?, headers?, historyApiFallback?, host?, hot?, http2?, https?, liveReload?, onAfterSetupMiddleware?, onBeforeSetupMiddleware?, onListening?, open?, port?, proxy?, public?, setupExitSignals?, static?, transportMode?, watchFiles? }
Помогло обновление версии Kotlin, но осадочек остался. С одной стороны, логично, что используется существующая экосистема JavaScript, но с другой — все ее проблемы едут вместе с ней. Печалит, что она не очень инкапсулирована.
Hot-reloading — прикольно, когда работает, но уныло, когда ломается. С учетом того, что для его работы требовалась перекомпиляция, которая иногда валилась с ошибкой, проще было сразу делать ребилд.
Было ожидание, что я пишу код “как обычно”, а взаимодействую с JavaScript только в отдельно выделенном загончике — слое интеграции. Однако
Например, хотел вынести шаблон строки в константу, чтобы потом вызывать String.format
, но фигушки — format
существует только в JVM-мире. Похожая история с регулярками: JavaScript не поддерживает матчинг-группы. Еще хотел с помощью рефлексии генерировать список необходимых полей в запросе — тоже нельзя, в JavaScript рефлексией почти ничего нельзя сделать.
Библиотеки
Кроссплатформенные библиотеки — это правильная и хорошая идея, но чувствуется, что они в очень ранних стадиях и заточены под конкретные кейсы. Я задолбался везде проставлять аннотации @OptIn
для экспериментальных фич: вроде как логично, потом проще будет выпиливать, а с другой стороны — компилятор-то на что? Поменяется API в новой версии — пусть не компилируется, а если компилируется — то пусть работает. Можно было бы помечать по аналогии с @Deprecated
: простое предупреждение, которое можно подавить, а не обязательно помечать.
Документация Ktor клиента оставляет желать лучшего, да и по коду тяжело разобраться. Шаг в сторону от единственного правильного способа — уже хлебаешь проблем.
Например, есть DSL для составления запроса и метод HttpMessageBuilder.accept(type: ContentType)
. Как туда прописать что-то нестандартное? Никак, в явном виде добавить append(HttpHeaders.Accept, CUSTOM_CONTENT_TYPE)
. При этом сам вспомогательный метод делает ровно то же самое, и добавить в DSL аналогичный метод, принимающий строку — нет проблем. Странно что это не сделали, смесь DSL и не-DSL выглядит стремно.
Похожая ситуация с авторизацией. Для нее вообще нет DSL, можно только добавить как заголовок. Почему? Потому что создатели посчитали, что единственный верный способ прописывания авторизации в клиенте — прописать ее при создании клиента. Причем поведение по умолчанию — посылать запрос без авторизации, а если словил 401 — перепосылать с авторизациией. Даже base64 не заиспользуешь из клиента — он недоступен наружу, пришлось копипастить.
Настроить путь запроса можно примерно тремя способами: get(url)
, get(куча параметров, но не все)
, get(){куча dsl})
, и каждый неудобен по-своему. Разумеется, можно еще эти способы комбинировать. Первый еще парсит URL: разбивает его на кусочки, чтобы потом собрать обратно. API для преобразования URL из строки в объект — URLBuilder().takeFrom(url).build()
, короче не нашел. Опять два стула: один с неполным DSL, другой с фабриками и билдерами. В тестах еще наткнулся на проблему, что объект Url
c путем /path
и path
— это два разных Url
, хотя полная строка выглядит одинаково. Очень весело такое было дебажить.
Сериализации есть куда расти. Для каждой DTO надо явно прокидывать @Serializable
, но это скорее хорошо, чем плохо.
Duration — основной поставщик @OptIn
в коде. Местами не очень логичен. Например, для получения Instant
с текущим временем нужно вызывать не Instant.now()
, а Clock.System.now()
. В целом впечатления смешанные: вроде удобно, но встречается неконсистентная логика. Joda Time все-таки качественнее будет.
Корутины
Это мой первый проект с корутинами. Я решил набить побольше шишек, да:)
Первая грабелька, на которую наступил — двухцветные функции. И это был осознанный шаг команды разработчиков.
А столкнулся я с этим в первый раз когда написал sequence
, на котором вызывался map
с suspend
-функцией внутри. Пришлось читать туториал про каналы и потоки (забавно, что в нем в качестве примера тоже используется работа с GitHub), чтобы выяснить, что мне подходит Flow
(холодный асинронный поток данных). И ладно бы надо было поменять тупо sequence
на flow
везде, но нет — все работает немного по-другому. Из позитивного — понравилась функция transformWhile
, которая отлично подходит для прерывания потока после получения последнего элемента. А вот генерацию потока с нуля в чисто функциональном стиле (как с generateSequence
) сделать не получилось. В итоге у меня в коде так себе кусочек с мутабельной переменной и do-циклом, причем даже рекурсию там скорее всего не получится сделать. Странно, что в стандартной библиотеке нет аналога generateSequence
.
Разумеется, вскрылись проблемы, связанные с JavaScript. Экспортируемую функцию нельзя сделать suspend
(хотя непонятно, почему). В JavaScript нет runBlocking
, что особенно больно в тестах, хотя конкретно для них есть какое-то шевеление. Вроде как это обосновывается тем, что можно иметь только один поток в JavaScript, но с другой стороны — есть же еще воркеры.
Одним из последних штрихов было добавление индикатора прогресса. Я хотел сначала попробовать разделить Flow
, чтобы изолировать основную логику от логики обновления процента выполнения. Вроде как мне нужен был SharedFlow
, но из официальной документации не понял, как его сделать красиво из обычного. Внезапно, самой понятной оказалась документация для Android.
Ладно, создал SharedFlow
, запустил основной поток в логику вычисления, второй — в индикатор прогресса… и ничего. Было выполнено несколько запросов, но результаты никто не обработал. В консоли пусто, ошибки нет ни на стадии компиляции, ни во время исполнения. Долго с этим разбираться не стал, хотелось сделать красиво, не получилось — ну и ладно. Сделал в итоге явную передачу объекта для потребления прогресса. Позже планирую сделать MWE и завести баг. Возможно это опять из-за однопоточности JavaScript, но хотелось тогда хотя бы получать нормальную ошибку.
Интеграция с JavaScript-библиотеками
Честно говоря, я охренел, насколько погано и неудобно работать с куками в чистом JavaScript. Решил подключить популярную библиотеку для этого, и это оказалось довольно просто. У Kotlin есть встроенная генерация типов из TypeScript-дефиниций. Жаль, что пришлось понизить версию библиотеки, потому что для нее не было актуальных дефиниций типов. Получилось вот так:
implementation(npm("js-cookie", "2.2.1"))
implementation(npm("@types/js-cookie", "2.2.7", generateExternals = true))
Видимость модуля глобальная, что немного печалит. Вляпался в отличия способов подключения модулей, пришлось явно указывать, что используется CommonJs — опять экосистема JavaScript течет через абстракции. Дальше возникла проблема с передачей аргумента в библиотечный метод: в библиотеке принимается сырой JSON-объект, а в сгенерированных определениях это был интерфейс без реализации. Попытался сделать по-честному: сделать реализацию интерфейса (получалось по-уродски, если честно). Однако получил в рантайме очень описательную ошибку:
TypeError: o[s].split is not a function
Самым простым решением оказалось пихнуть сырой JSON. Типизация это хорошо, но в данном случае она только добавляет хлопот.
Расстроило, что мало реализовано преобразований. Например, как сконвертировать сырой JSON в Map? А никак, только руками. Примерно та же история с преобразованием HTMLCollection
в нормальный список — там проще, но элементы надо явно преобразовывать к HTMLElement
.
Основная логика
Inside every large program is a small program struggling to get out (Tony Hoare).
Основная логика была самой простой частью. Считать перцентили — легко, но вот обобщить — не очень. Внезапно я обнаружил себя читающим всякие статьи по поводу того, как правильно называется группа, позволяющая считать среднее. К сожалению, у Kotlin’а даже нет обобщений над Number
, не говоря уже об утиной типизации или классов типов, хоть на это и есть причины. Пришлось имитировать, получилось немного страшновато.
При обработке посмотрел еще раз на Result
— и это уныние, кастрированный Either
, который не получится даже нормально сматчить через when
, и опять возникают проблемы с “двойным nullable”. Да, есть альтернативы, и можно написать свой, но я решил доупороться и использовать все стандартное.
При работе с индикатором прогресса наткнулся на проблему, что не знаю, как посчитать количество пулл-реквестов без лишнего запроса. Пришлось перейти с REST API на GraphQL API. Сначала пробовать сделать запрос через поисковый запрос
{
search(query: "type:pr repo:jetbrains/kotlin state:closed", type: ISSUE, first: 100) {
issueCount
edges {
node{
... on PullRequest {
url
createdAt
mergedAt
state
}
}
}
}
}
но обнаружил, что количество расходится с аналогичным при REST-запросе. Зато при запросе к коллекции все совпало.
{
repository(name: "kotlin", owner: "jetbrains") {
pullRequests(first: 100) {
totalCount
nodes {
url
createdAt
mergedAt
updatedAt
closedAt
state
}
}
}
}
Уж не знаю, с чем это связано, но тратить время на исследование этой проблемы не захотелось.
Увы, GitHub GraphQL API пока не работает без авторизации. Это ставило под вопрос демо (да и работу было жаль выкидывать), поэтому я решил оставить оба клиента: для неавторизованных запросов использовать REST с приблизительной оценкой количества, а для авторизованных — GraphQL.
Кроссплатформенной библиотеки для работы с GraphQL из Kotlin я не нашел, поэтому запрос выглядит страшновато, но адекватных альтернатив я не придумал. Выяснилось, что объекты пулл-реквестов в REST API и GraphQL отличаются: в REST используется snake_case, а GraphQL — СamelCase, и немного отличался enum статуса, но это решилось тупеньким конвертером.
Классной фишкой GitHub GraphQL API оказалась скорость: запрос через него выполняется за 200-500 миллисекунд, в то время как через REST — от двух секунд. В целом работа с GraphQL оставила приятные впечатления.
Веб-морда
Интерфейс получился довольно скучный, с рисованием у меня, увы, плохо (я же не фронтендер). Немного не очевидны были моменты с автозаполнением формы, действием при нажатии на Enter и обработкой ошибок ввода, но это все было вызвано моей неопытностью.
Для генерации использовал kotlinx.html. Не понравилось, что нужно вызывать document.create
для создания элемента, и что для присвоения в innerHtml
полученный объект надо преобразовывать в строку, в то время как append
нормально добавляет элементы. Унарный плюс для добавления текста был довольно не очевидным моментом, причем это не вызывает ошибку компиляции — просто выводится пустое содержимое, потому что строка используется как имя класса.
Тесты
Поддержка тестирования расстроила. Тут начало всплывать много особенностей JavaScript.
Нельзя использовать названия методов с пробелами, как это обычно делают в unit-тестах на JVM. Т.е. вместо fun `should fetch single page properly`()
надо писать fun should_fetch_single_page_properly()
. Вроде мелочь, но неприятно.
С корутинами была уже упомянутая проблема, что нет возможности запустить тест через runBlocking()
, в итоге пришлось использовать костыль с GlobalScope.promise
:
@OptIn(DelicateCoroutinesApi::class)
fun runTest(block: suspend (scope: CoroutineScope) -> Unit): dynamic = GlobalScope.promise { block(this) }
и оборачивать явно каждый тест. Вроде можно попробовать альтернативные способа запуска тестов, но, опять же, хотелось пощупать стандартные инструменты.
Тесты запускаются через фреймворк Karma в реальном браузере, причем не в каком-нибудь, а в том, который явно указан в конфиге, один из популярных на выбор. Как следствие, возникла проблема с подгрузкой ресурсов из файлов. Очень грустно, что подобное не работает из коробки. require("fs")
не работает в браузере. В итоге пришлось извратиться с window.fetch
и конфигурацией Karma, который надо настраивать через исполняемый js-файл в специальной папочке (sic!). Но даже с таким подходом все равно были проблемы с корутинностью — ловил стремную ошибку:
А все потому, что надо было выполнять операции в корутине. Но корутины в JavaScript нельзя вызвать с блокированием, поэтому в итоге получился синхронный запрос через XMLHttpRequest:
fun loadResource(resource: String): String {
XMLHttpRequest().apply {
open("GET", "/base/$resource", async = false)
send()
return responseText
}
}
Запускаются все тесты скопом, даже если указал один метод или один класс. Позже понял, что проблема с пакетами, и если заменить в настройках теста полное имя на обычное, то прогоняется ровно 1 тест, но тогда отваливается интеграция с Intellij :( С другой стороны, она оставляет желать лучшего: дебажить через нее у меня так и не получилось. Такое подозрение, что класть код в пакеты не очень почетно, потому что нельзя иметь тесты с одинаковым именем класса, даже если они в разных пакетах. Запуск при этом ругается на то, что не может загрузить временную папку :/
С точки зрения ассертов Kotest работает просто замечательно, не зря я его выбрал победителем среди прочих библиотек.
Методы для тестирования Ktor клиента оставляют желать лучшего. Тестирование запросов через мок — классно, но тогда надо прокидывать явно MockEngine
, который теперь не получить автоматически для платформы. Более того, MockEngine
конфигурируется только один раз и удобно можно только задать последовательность ответов, поэтому пришлось навертеть фабричных лямбд. Конечно, MockEngine
можно сделать глобальным, и переиспользовать, меняя мутабельный конфиг, но изначально он должен быть непустым. Опять DSL создает ограничения, а не помогает. Вообще API не очень человечное: например, если надо матчить запрос (что он с правильными параметрами идет) — пиши обертку. Какой-нибудь HttpRequestData
содержит executionContext
от корутин — офигенно конечно матчить то, что по факту в дата-класс можно превратить. Были проблемы и с обычными методами. Помимо упомянутой проблемы с парсингом URL, которую очень весело было искать без дебага, метод toString()
у объекта ответа выводит только первые несколько символов у содержимого — спасибо, теперь надо писать колбасу
(request.body as TextContent).bytes().decodeToString()
для отладки. Функция для получения содержимого в байтовом представлении — асинхронная, поэтому ее не используешь просто так.
Заключение
Я не настоящий сварщик, поэтому некоторые проблемы, с которыми я столкнулся, могут показаться детскими для матерых фронтендеров. С другой стороны, кажется, я как раз попадаю в одну из целевых ниш Kotlin-JS: человек, который плохо знаком с JavaScript, которому нужно написать что-нибудь на нем, желательно типобезопасно.
На мой взгляд, идея неплохая, но реализация немного подкачала. Считаю, что для прода Kotlin-JS не очень готов, особенно если учитывать, как больно его тестировать. Ожидал, что инкапсуляция будет получше и что особенности JavaScript не будут везде торчать.
Поиграться с результатом можно тут, а почитать код — на GitHub.
В будущем планирую сделать красивый индикатор загрузки (потому что <progress>
HTML5 не позволяет универсально отобразить проценты), поковыряться с SharedFlow
, и попробовать Kotlin MultiPlatform — портировать проект на Native.
Задачи: делить или не делить? (или "У меня бомбит от скрама")
Disclaimer: многие вещи намеренно гиперболизированы. Структура заметки стремная и похожа на поток токсичного сознания.
Плюсы
В последнее время есть тренд на деление задач на максимально мелкие подзадачки. См., например, доклад. С одной стороны, выглядит довольно логично: слона-то надо есть по частям. Плюсов довольно много, мелкую задачку:
- проще сформулировать
- проще ревьюить
- проще мержить
- проще оценить
- проще понять
- можно кинуть на новенького
- легче параллелить между разными людьми
- да и вообще, конвеер получается, проще менять шестеренки!
С другой стороны, сейчас разработчик во многих компаниях снова становится кодером (или просто продолжает?). В типичном скраме задачка рождается в недрах разума “подставок для бифштекстов”, фильтруется “продуктом”, жуется аналитиком и выплевывается в Jira-таск: накодь-ка мне, пожалуйста.
Потом другой разраб посмотрит (при хорошем раскладе), и дальше поедет через “леди баг” на “шило”. Все, что не касается собственно кодирования — в топку, делай, что говорят, но потом спросят на “смотре выступлений”: “а как ты повлиял на наши бизнес-OKR и KPI года по выручке”. Чтобы сильно не вякали, можно дать щедрых 20% времени, незанятого встречами, на “техдолг” (но только если не будет задач поважнее, и “техдолг” может быть таким, что это фича).
Минусы
Если разбить обычную задачу на мелкие, ее очень легко сделать не до конца: 20% усилий ради 80% результата, остальное лежит гнить в бэклоге, который может никто и не разгребать (да и зачем, когда продакт знает, где горит сейчас у бизнеса и что ему нужно). А еще когда бэклог из мелких задачек — отличный повод сказать “ой, там так много задач, давайте сфокусируемся на тех, что в приоритете, я их и так знаю”.
(картинка немного баян, но осознал я это слишком поздно, shame on me)
Так одну задачку можно три раза заводить — потому что про предыдущие две копии никто уже не помнит. Нет видения цельной картины, исполнитель не всегда понимает, зачем он ее делает, от этого страдает качество. Причем это может возникнуть как и в случае, когда над одной большой задачей работают несколько человек (в т.ч. потому что каждый делает немного по-другому), так и один (по частям задача выглядит хорошо, целиком — зависит от того, кто разбивал на тикеты, обычно это не исполнитель).
Кстати, про цельную картинку: где архитектура в Скраме? Ответ в рифму, и она “появляется сама”, с учетом того, что скрам это про то, как быстро сделать как-нибудь, чтобы потом переделать нормально (когда-нибудь через никогда). Можно, конечно, завести отдельную задачку “на архитектуру”, но, во-первых, это “не настоящий Скрам”, во-вторых, маловероятно, что будут достаточно проработаны все требования (это же не водопад, чтобы заранее знать стратегический план, “вижена” вперед на одну-две итерации хватит).
Разбиение задачи на кучу мелких не защитит от рисков. Во-первых, о чем-то можно тупо забыть. Во-вторых, многое возникает на середине пути (если это не совсем спинно-мозговые задачи). Поэтому да, сами мелкие задачи проще оценить, но это не значит, что большая задача будет оценена точнее.
При частом переключении между мелкими задачами теряется не только общее видение, но и фокус. Сегодня мелкая задача в одном микросервисе, через полдня (час?) в другом, переключение контекста — это не бесплатно. Тут сам с собой поспорю, потому что делать задачи в одной области кода может легко надоесть, поэтому важно найти золотую середину. По моему опыту она лежит ближе к большим задачам. Признаю также, что как руководитель, мог бы и получше следить за этим балансом в своей команде на старой работе :( Фокус теряется и в формулировках: приходится копипастить описание или заглядывать в эпик.
Мелкие задачи приводят к тому, что разработчик разучивается мыслить. В задаче все уже максимально разжевано — зачем напрягаться? Вот только когда попадается задача на исследование, в которой надо делать что-то необычное или просто высокая степень неопределенности, то она превращается в Большую и Страшную Задачу на Много Сторипоинтов, Которую Надо Поделить (даже если это бессмысленно). И делать ее после кучи мелких действительно тяжело, тяжко говорить на дейли “занимался своей Задачей”. Хочется закончить с ней побыстрее, а не покачественнее. А поскольку обычно не разработчик делает тикет, то и навыки декомпозиции тоже страдают. В том числе нормальное разбиение на коммиты. Тяжело ревьюить не большие задачи, а большие коммиты-свалки.
Следствие проблемы большой задачи среди маленьких и отсутствие видения полной картины — тяжело вносить что-то новое. Просто потому, что это “большая” задача, а надо быстро, некогда экспериментировать, вагон задач ждет своей очереди. В итоге тяжело развиваться, потому что негде думать и нет ничего нового.
Если несколько человек работает над большой задачей, разбитой на мелкие кусочки, это не обязательно означает, что будет больше бас-фактор или что больше человек будут понимать весь код, связанный с задачей, целиком. Повысить бас-фактор, вообще говоря, можно нормальной документацией, код-ревью и пометками в тикетах.
Кстати, мелкие задачи не мотивируют отписываться в тикетах. В большинстве мелких задач это не нужно. Но вот в более-менее крупных задачах имеет смысл писать о неудачных решениях, возникших проблемах и т.п. Это поможет в будущем понять, почему было сделано “именно так”, а не “иначе”. Обычно эта информация передается из уст в уста на дейли (разумеется, завтра это уже никто не вспомнит).
Разбиение на мелкие таски в пределе — это создание тикетов, которые дольше писать, чем делать. Чтобы он полежал в бэклоге, чтобы его прогрумили (если есть такой процесс, конечно), приоритезировали, оценили “вэлью”, оценили в “сторипоинтах” планнинг-покером… Хотя это мог бы быть просто коммит.
Наконец, от мелких задач нет чувства достижения. Работать в команде — это хорошо, но хочется и самостоятельно что-то из себя представлять. Что говорить на итогах года? Сделал 100500 тикетов, которые попались по очереди? Писал код? Как следствие — легче “забить” на некоторые шероховатости: код-то общий, даже если ты его вылижешь, мало кто это оценит.
Может, в скраме отказались от инженерного проектирования ПО и вся индустрия это просто “авантюра с кодом”?
И чо?
Безусловно, можно делать хорошую работу и в скраме с мелкими задачами, особенно если команда классная (хотя скорее вопреки, да и процесс надо модифицировать под свои обстоятельства).
Но я бы смотрел на вопрос по-другому. Основная цель разработки — это все-таки получение работающего (надеюсь, что слово “качественно” здесь подразумевается) решения. Фокус, соответственно, должен быть на достижении результата, а не на том, как его достичь.
Для меня тикеты — это способ повышения прозрачности проекта и управления разработкой. Это история разработки, средство отчетности и банально средство записи того, что надо еще сделать. Декомпозировать задачи нужно, но считаю, что разбивать задачи на подзадачи — зона ответственности разработчика-исполнителя (начинающим помогут старшие товарищи). Как он делает (отдельными тикетами, комментариями в тикете, чек-листами и т.п.) — не так важно, лишь бы это было в трекере и читабельно (потому что он делает это не только для себя), а не на словах на дейли или вообще непонятно где. Но при этом не надо возводить владение кодом совсем в абсолют, и делать его сильным: да, экспертиза будет бешеной, но может надоесть.
Может, это я уже старый дед и не хочу учиться новым трюкам, но все-таки хочется делать цельные задачки, а не кусочки…
Про продуктивность
По моим ощущениям, IT живет в культе продуктивности и эффективности. Это в целом неплохо, но тут очень легко перегнуть палку.
В начале пути это еще нормально. Нужно набрать крейсерскую скорость, чтобы выполнять все рабочие задачи эффективно, научиться видеть перспективу, понимать принципы вместо того, чтобы тупо применять практики, овладеть системным мышлением. Инвестировать в себя: развиваться, накапливать кругозор, отточить технику.
Потом надо будет героически преодолевать прокрастинацию, продолжать изучать что-то новое, чтобы быть в тренде, бороться с синдромом самозванца (“вон какие крутые чуваки есть в интернете, а я — лох”), найти 100500 блогов, которые надо читать, изучить наконец английский нормально, прочитать еще одну умную книжку про распределенные системы или функциональное программирование, добавить еще один труп на кладбище пет-проектов…
Бизнес зачастую тоже хочет максимальную утилизацию ресурса, да и его понимание agile тоже к этому подталкивает. Разработчик, сидящий без задачи — преступление. Нечего сказать на ежедневном митинге — стыдно, гоните его, насмехайтесь над ним. Что? Какой еще техдолг, там бизнесу надо 100500 новых фич, у них есть просчитанная миллионая ценность, их ждут с прошлого года, а этот ваш долг подождет, не в приоритете. RnD? Не, рисково, мы используем только проверенные технологии. Когда будет 100500 специалистов с этим навыком, тогда может попробуем, мы не лохи сырое использовать. Лучше поучаствуй на этом митинге, где мы будем говорить ртом, потому что нам некогда читать.
В общем, не надо тратить каждую минуту полезно. Это касается как работы, так и жизни.
С точки зрения работы — в ней нужно уделять время на развитие как специалистов, так и проекта. Если времени на это постоянно не хватает — это красный флаг сломанных процессов, планирования и управления.
В личной жизни нужны паузы для усвоения информации, чтобы мозг смог уложить мысли в голове и обработать их — спорт, прогулки и медитации в том числе про это. Нужно регулярно бывать оффлайн наедине со своими мыслями. Может быть скучно или страшно, но оно стоит того. Скучать, кстати, тоже полезно: чтобы сделать кому-то хорошо, надо сделать ему плохо, а потом вернуть “как было” :)
Я заметил, что раньше у меня для этого единения с собой был транспорт, обед (когда надо ножками топать в жральню), душ, отпуск и дача, где не было интернета. Переход на удаленку — это хорошо и экономия кучи времени, но такая “эффективность” может быть ложной: это время может заполнится и чем-то не тем. Транспорт перестал быть актуальным, да и рабочий телефон искушает наличием интернета. Ходить обедать теперь никуда не надо — еда уже дома. На одной даче появился интернет (работать надо), в отпуске тоже вечером залипаю в интернете, гостиница без WiFi — это моветон. Даже в ванной стал лежать с планшетом. А были времена, когда к интернету надо было подключаться осознанно и по необходимости — было чем заняться и без него.
От всего этого чувствуешь себя белкой в колесе или Алисой, которой надо бежать еще быстрее, чтобы оставаться на месте. А чувствуется, что надо как раз остановиться, отключиться от мира и подумать.
Что иронично, пишу я эту заметку в воскресенье, на даче, с рабочего ноута, будучи подключенным через интернет с рабочего смартфона. Чтобы “эффективно использовать” свое время, пока насос качает воду из подвала. Не надо так:)
P.S. Некоторые ситуации в первой половине этой заметки я намерено утрировал, чтобы ярче выразить мысль. Но “основано на реальных событиях”, как говорится.
Впечатления от F#
… на основе обзорного тура и аж одной программы. Ну ладно, еще есть паттерн railway oriented programming, с которым я познакомился довольно давно, и в котором примеры на этом языке. При этом с С# у меня опыта чуть побольше, правил наш замечательный толстый клиент и в студенчестве даже делал приложение для Windows Mobile (в те времена, когда использовался термин “коммуникатор”). Так что не стоит ждать от этой заметки чего-то глубокого или интересного: уровень гораздо ниже, чем у хабровской стрельбы в ногу с Kotlin.
Если есть желание быстренько ознакомиться с синтаксисом, но лень читать даже обзорный тур, то есть F# за 60 секунд.
Установка и среда разработки
Сам dotnet устанавливается через snap, особых проблем не возникло. Напряг момент с телеметрией, которая по умолчанию собирает много чего. К счастью, можно ее отключить через переменную окружения (или отключится только уведомление, это надо еще проверить:) ) , но первые N запусков с ней уже были. И узнать о ней можно либо из документации (которую я разумеется прочел уже потом) или после первого запуска. В общем, в бесплатном приложении товар — это пользователь, классика.
Дальше начались пляски с VS Code. Вроде как из каждого утюга хвалят, да и альтернатив не особо много. Скачал, поставил, установил рекомендуемые плагины. Сгенерировал hello world и успешно запустил его. Решил поправить немного — а мне VS Code такой: а у тебя F# не установлен.
Ладно, чешу репу, использую 2 стандартных приема… И все равно не могу запустить даже стандартный hello world. Оказывается, надо было воспользоваться “простой” инструкцией и отредактировать руками пару файлов проекта VS Code. Я, конечно, все понимаю, это блокнот с наворотами и все такое, но для обычного шарпового приложения пришлось бы пройти через такой же путь, а это продукты одной компании! Вроде в 2021 году живем, чтобы руками файлы билдов править — спасибо хоть, что они не в формате Makefile.
Изучал все не в один присест и когда после перерыва продолжил писать код — запуск опять перестал работать, но по другой причине: VS Сode не мог понять конфигурацию coreclr
. Предлагал поставить расширение, чтобы ее поддерживать, и единственный кандидат в каталоге расширений — это “extension for PeachPie - the PHP compiler for .NET and .NET Core”. W T F. Удалил все плагины, перезапустил VS Сode, установил заново расширения — не работает. В интернетах советуют… переустановить плагины. Сделал еще раз те же действия — заработало. W T F.
С точки зрения самой разработки — есть какая-то подсветка, которая ощутимо подтормаживает. Автодополнение есть, но не очень впечатлило, подсовывает фигню какую-то (очень субъективное ощущение, но как есть). Иногда раскрываются выведенные типы, но они тормозят вместе с подсветкой.
В общем, я получил больше негативных эмоций, чем удобства разработки. Гемора много, а толку мало.
Интересности
Весь язык описывать не буду, опишу вещи, которые показались интересными, странными или важными.
Синтаксис
Отступы и значимое форматирование как в питоне — на первый взгляд прикольно. Однако даже с этим код местами все равно может выглядеть стремно, даже в туре есть такие места (например, вторая секция кода в разделе паттерн-матчинга). При этом иногда можно через ;
записать несколько выражений в одной строке (хорошо хоть, не всю программу). Некоторые источники говорят, что скобки и точки с запятой “не засоряют” код — имхо, это сильное преувеличение, использовать их все равно нужно, и я бы не сказал даже, что редко.
Часто торчит наследие C и C#: например, “процентные” форматы вывода в sprintf. При этом в сравнениях используется непривычный синтаксис: <>
вместо !=
и not
вместо !
— кто-то не любит восклицательный знак. А вот and
и or
это по-прежнему &&
и ||
.
Объявления функций похожи на Haskell’овские. Однако любую рекурсивную функцию надо явно помечать — это как-то тухло и непонятно зачем. Еще стремно, что для лямбд используется ключевое слово function
(хорошо хоть его можно сократить до fun
). Функция без аргументов неотличима от обычного выражения (и это хорошо), поэтому для грязных функций без аргументов надо явно передавать unit (скобочками).
Для классов используется странный синтаксис, но что-то в этом есть: меня заинтересовала идея, когда реализация интерфейса явная и локализованная. Вместо override
для всех методов нужно написать, мол, реализую интерфейс такой-то: этот метод так, этот сяк. Хотя это стоит пощупать: может, на бумаге хорошо, а в реальности будет как с проверяемыми исключениями в Java.
Понравилось ключевое слово use
для ресурсов. Достаточно инициализировать/открыть ресурс где надо, а закроется он сам при выходе из текущей области видимости — никаких дополнительных оборачиваний не нужно.
Классная фича — единицы измерения на уровне синтаксиса языка, причем для системы СИ (которая международная система единиц) все единицы уже определены. Был бы я студентом и писал бы решение какой-нибудь типовой задачи по физике — обязательно попробовал бы.
Работа с коллекциями
Списки объявляются через точку запятой: [1; 2]
. Альтернатива — по элементу на строку, т.е. разделитель — перенос строки. Судя по всему логика тут в том, что ;
и перенос строки “взаимозаменяемы” (на самом деле нет). А запятая — разделитель кортежа, т.е. [1, 2]
— это то же самое, что и [(1,2)]
.
Все полезные функциональные методы — статические. Никаких тебе someList.filter someLambda
, вместо этого надо писать List.filter someLambda someList
. Когда речь идет о преобразующих функциях — это еще ладно, но вот когда приходится писать Seq.head generator
вместо generator.head
— это уныло. Местами даже проще было использовать низкоуровневый итератор (Enumerator). Вообще статические методы — это грустно, поменяешь тип коллекции — и всю цепочку переписывать. При этом extension-методы есть, шаблонные типы есть — вроде ничего не мешает реализовать абстрактные map
, filter
и т.д. Может, это “так исторически сложилось” (extension-методы были в F# не всегда), может, система типов недостаточно мощная, но от этого приятнее не становится.
Есть сахарный метод конвеера, |>
: подобно unix’овому pipe |
, с его помощью можно передавать результат выполнения предыдущей функции в следующую. Например
[1;2;3] |> List.filter (fun x -> x % 2 <> 0) |> List.map ((*) 5)
выдаст [5; 15]
. Можно еще сделать композицию функций через >>
, но применимость ее в цепочках, на мой взгляд, сомнительна.
Массивы определяются через интересную комбинацию [| |]
. Разочаровало, что они по умолчанию изменяемые, хотя списки — иммутабельные. Неконсистентно, однако.
Странно, что в туре не было словарей и множеств. Нативные для F# можно сделать из списка с последующим преобразованием. Вообще инициализация коллекций не всегда очевидна.
Немного чужеродно выглядит обычный for
, когда есть итераторы. Местами читается как SQL-style LINQ, т.е. погано.
Особый интерес вызывают имена некоторых методов. Как вам, например, foldBack2
, intersectMany
, map3
, mapi2
, zip3
(когда нет zip2
), sortInPlaceBy
vs sortInPlaceWith
vs sortWith
? Еще позабавило, что в ответах на SO иногда рекомендовали использовать LINQ для некоторых преобразований, особенно если начало или конец — C#-коллекция.
Алгебраические типы данных
Реализованы интересно, но странно, что нет из коробки перечисления и строкового представления (они по факту POCO). Забавно, что произведение типов можно записать через умножение.
Single-case Discriminated Union — с одной стороны прикольно, а с другой — это просто альтернативный синтаксис для record, как POCO с одним полем, который особо нового ничего не привносит. Для снятия обертки предлагают использовать паттерн-матчинг, стандартного метода класса вроде как нет.
Взаимодействие с C#
При вызове чего-то шарпового возникает немного вырвиглазная ситуация: часть методов (C#) в UpperCamelCase, а часть (F#) — в обычном camelCase. А стандартная библиотека общая, так что этого не избежать.
С# список и F# список не совместимы: попробовал создать шарповый, чтобы потом по нему map
’ом пробежаться и меня постигла неудача. Надо конвертировать через Seq.toList
.
Некоторые источники утверждают, что в F# есть null-safety. Это, увы, ЛПП: это гарантируется только в “чистом” F#, вызвал любую С# библиотеку (в т.ч. стандартную системную) — жди беды или обрабатывай явно.
Итого
Какой-то киллер-фичи не нашел (всякая асинхронщина и ФП сейчас почти в каждом утюге). Не могу сказать, что язык удобный, да и с консистентностью у него есть проблемы. Без C# тяжело что-то написать. Вообще напоминает ситуацию со Scala. В ней тоже сильна ФП-парадигма, многие вещи приходится делать через Ж…аву, причем интероп иногда фиговый, свои коллекции, да и изначальное позиционирование было как “ФП на существующей экосистеме”. Но у Scala есть какая-никакая ниша, работа с коллекциями там приятнее (особенно в 2.13/3.0), сам язык логичнее, экосистема богаче и нет большого брата с телеметрией. В общем, в F# есть интересные идеи, но использовать его я, конечно, не буду :)