#47: Fleet: редактор с оптимистичными транзакциями
Александр Пахомов и Андрей Зайцев (инженер JetBrains, архитектор редактора Fleet) разбирают внутреннее устройство Fleet как одновременно редактора кода и мутабельной базы данных. Разговор идёт от трёх исходных челленджей (remote development, коллаборация, полиглотность) через `React`-подобный UI, чистые функции и персистентные структуры данных к главной идее — распределённым оптимистичным транзакциям на механике «переписывания истории» в духе git-rebase, где всё состояние редактора это одно значение. По пути — параллели с `Datomic` и `MVCC`, детектор конфликтов на хэшах, нативный рендеринг через `Skia` и бюджет кадра при 120 FPS, а также роль garbage collector в функциональном коде.
Главное
- Всё состояние Fleet — это одно персистентное значение (мутабельная база данных в памяти в духе `React` + `Redux`/`Re-frame`); UI это чистые функции от этого значения, а транзакции меняют значение отдельно от отрисовки.
- Fleet задумывался как ответ на три челленджа, которых не было у IntelliJ IDEA изначально: remote development, совместная разработка над одним workspace и полиглотность (быть редактором «для разработчиков», а не для конкретного стека).
- Главная идея быстрого UI — computational avoidance: по максимуму не звать пользовательский код; движок знает, какие компоненты какие части базы читали, и перевызывает только те, чьи данные изменились.
- Совместное редактирование реализовано не через `CRDT` и не через operational transformation, а через путешествие во времени и `git-rebase`: транзакции это чистые функции, которые можно переиграть поверх нового состояния.
- Конфликт определяется не записью в одно место, а нарушением причинно-следственной связи — тем, что для решения о записи ты прочитал то, что записал другой; детектируется пересечением множеств хэшей (read/write set), почти как блокчейн.
- Персистентность базы даёт чтение без блокировок (не нужен read-лог), позволяет гонять массу фоновых рутин против состояния и так борется с фризами; bottleneck остаётся единственный транзактор, применяющий функции по очереди.
- Fleet рисуется как игра — каждый кадр весь экран заново: UI-фреймворк строит список draw-call'ов и одним махом отправляет их на GPU через `Skia`; бюджет кадра при 120 FPS около 8 мс, из них в userspace остаётся ~4–5 мс.
- В функциональном коде аллокации происходят «как дыхание», поэтому GC с компактящим молодым поколением часто выигрывает у ручного `malloc`/`free`; а место, которое много аллоцирует, обычно и есть медленный код.
В выпуске
- Андрей Зайцев — Инженер JetBrains, стоял у истоков редактора Fleet и является его архитектором; ранее работал над IntelliJ IDEA и код-браузером Absource. de.linkedin.com ↗
Ссылки
Расшифровка
[00:00] Александр: Здорово! Меня зовут Саша Пахомов, и я инженер, который любит своё дело. Вы слушаете подкаст, в котором разработчик современной базы данных изучает то, как они работают, и делится знаниями со слушателями. В предыдущем выпуске вместе с Никитой Прокоповым мы круто обсудили редакторы кода. И тогда я выразил своё желание разобраться в том, как работает новый редактор кода от компании JetBrains — Fleet. Вот вам отрывок диалога с Никитой.
[00:27] Никита: В целом проект технически интересный, и мне работа над ним была особенно интересна. Мы сделали кастомный рендер — я его делал на Skia, графической библиотеке от Google Chrome и Android, мы её притащили в Java. У нас там свой UI-фреймворк, всё полностью кастомное: нет ни Swing даже, ни веба, не дай бог. Все UI-компонентики рисуются с нуля, все менюшечки, тексты с нуля рисуются — что довольно прикольно. Архитектурно он очень интересный, но, видишь, это tough sell, не так легко заманить людей на такие идеи. Большинству людей нужно просто открыть файл локально, и сложно предложить им какие-то мега-улучшения.
[01:07] Александр: Ты очень интересно затронул тему UI на Java, Swing и Skia и того, как это сделано во Fleet. Мне, как человеку с супер-задротским подкастом про базы данных, это дико интересно — но мне кажется, что если мы начнём про это говорить, то это будет просто бесконечный разговор.
[01:23] Александр: И вот после выпуска с Никитой ко мне пришёл Андрей Зайцев — человек, который стоял у истоков Fleet и является архитектором этого продукта. Сегодня мы поговорим про Fleet. Будет интересно. Поехали.
[01:42] Александр: Получается, что ты один из тех людей, кто, насколько я понял, заварил движуху с Fleet и начинал с самого начала. Что стало тем поворотным моментом, в который ты понял, что надо делать что-то другое? Чем не устраивал и не устраивает IntelliJ IDEA?
[02:14] Андрей: Хотелось подумать над тем, как редактор мог бы быть устроен, если бы мы писали его с нуля — зная о тех требованиях и челленджах, которые встали перед IDEA со временем. Первое такое требование — появление Remote Development. Это способ разработки, когда всё, что ты запускаешь и собираешь, находится не на локальной машине, а где-то там: в контейнере, в дата-центре. Тогда все разрабатывали локально, и было понятно, что делать. А потом возникла потребность разрабатывать где-то на сервере, в клауде, в контейнере — причём, возможно, не одному человеку, а нескольким. И это новое требование концептуально не вяжется с той архитектурой, которая есть в IntelliJ IDEA.
[03:08] Александр: В каком смысле?
[03:09] Андрей: Ты сейчас, сам того не заметив, назвал ещё одно требование — многопользовательскую историю. Над одним workspace может вместе, буквально в режиме pair-programming, работать больше одного человека. Это второй челлендж. Кроме того, с тех пор сильно изменился ландшафт движков для анализа кода. Помимо IntelliJ IDEA и всего её language support, есть отдельный огромный бранч нашей же компании, который занимается поддержкой C# и всего .NET-тулчейна — ReSharper. Его бы тоже сюда приклеить. А задача его приклеить рождается из того, что сильно изменился ландшафт разработки софта вообще. С появлением open source, который тогда не был большой вещью, стало больше проектов, где языки и стеки перемешаны как попало. Оказывается, существует любая комбинация стеков, на которых пишется проект: ты можешь придумать любую комбинацию технологий — и обязательно найдёшь проект, который именно её использует.
[04:04] Андрей: Это ещё одно требование к редактору, каким он был бы, если бы его писали сейчас: быть более flexible и менее opinionated относительно того, что вообще такое проект. Мы не делаем IDE для Ruby, не делаем IDE для фронтендеров, не делаем IDE для Java-разработчиков — мы делаем IDE для разработчиков. Отчасти это фактор, из-за которого VS Code так популярен: если всё отбросить, это редактор, который открывает какую-то папку и помогает тебе настолько, насколько может. Поставишь плагин про Java, он увидит, что вот здесь очень похоже на Maven-проект, — и начнёт как-нибудь помогать. Это желание встретить разработчика там, где он есть, в одном редакторе — без необходимости ставить много стек-специфичных продуктов.
[04:50] Андрей: Есть ещё несколько аспектов, которые были челленджем для IDEA. Во-первых, UI-стек. UI-стек IDEA сильно ограничивает её в том, что вообще можно делать с интерфейсом, потому что это Java и Swing, написанный в глубоких 90-х. Команда IDEA очень хорошо справляется с тем, чтобы заставить Swing работать так, как нужно, — огромный респект чувакам, кто этим занимается. Но нам хотелось попробовать что-то другое: что-то более современное и более перформантное. Ещё один аспект — фризы. В те далёкие времена конкурентность была не так важна, параллелизма особо не было — не так много ядер на машине, чтобы их надо было сильно утилизировать. Поэтому изначально в IDEA не было никакой поддержки многопоточности, она туда была довольно остроумно заэлекторофичена постфактум — большой респект за такую придумку. Но у неё есть свои импликейшены: глобальный read-write-лок на всю модель так или иначе может легко приводить к фризам, просто потому что user-space-код берёт этот лок и делает не пойми что, — особенно если мы говорим про плагины. Это был ещё один челлендж, на который очень хотелось ответить: как сделать программу так, чтобы она не фризилась.
[06:07] Александр: Получается три основных монументальных ограничения. Первое, которое мы уже хорошо проговорили, — remote development. Второе — коллаборативная разработка. Третье…
[06:26] Андрей: Полиглотность.
[06:27] Александр: Да, полиглотность. Это, грубо говоря, бизнес-часть — прям штуки, которые можно идти и продавать. А новый UI… нам, разработчикам, конечно, приятно, когда там на сях что-то написано или на Rust, но, грубо говоря, на продажу это, мне кажется, не так сильно влияет. Хотя я могу ошибаться, я не разработчик. И две более технических штуки, которые ты назвал: наличие Swing в IDEA и старых технологий, которые тормозят не только производительность, но и вообще возможности развития, а значит удорожают разработку. И последнее — фризы: то, как рендерится IDEA, позволяет внешнему коду, чаще всего плагинам, сделать так, что вся IDEA зависнет. Вот эти пять вещей сразу были понятны как основные, которые надо фиксить, — или всё-таки в начале была одна-две-три, а остальные докинулись потом? Была ли какая-то одна главная штука, из-за которой надо делать что-то новое, — или все пятеро сыграли?
[07:40] Андрей: Сложно вспомнить, как именно шло развитие мысли. Если говорить лично про меня, а не про мотивацию компании за этим продуктом, — когда-то я работал над другим продуктом компании, который назывался Absource: такой код-браузер и код-ревью-тул. Когда была первая технологическая демка Absource, я был очень эксайтед. Демка состояла в следующем: это был просто URL в имейле, ты его открываешь — и в браузере появляется код. Слева был список всех коммитов в репозитории; ты мог ткнуть на любой, увидеть дерево файлов в этот момент времени, открыть любой файл, сделать go to declaration или find usages. И всё это работало в рамках выбранной ревизии. Для этого использовалась IDEA под капотом — как-то специальным образом приготовленная, вывернутая наизнанку IDEA. Я был очень эксайтед, потому что думал: вот оно, будущее. Мы сейчас напишем IDE, которая будет работать в облаке, вообще без десктопа: просто открыл в браузере ссылочку — и пошёл программировать. С этого начались мои эксперименты, и начались они в области написания фронтенда к IDE. Absource существовал, но его фронтенд не был фронтендом для IDE — это был минимальный интерфейс, который можно нарисовать в браузере, чтобы показать код и какой-то минимальный UI.
[09:00] Андрей: Очень хотелось построить новый фронтенд к какой-то абстрактной IDE будущего. Первое требование было, чтобы он работал в браузере, — и из него родилась необходимость другой архитектуры: отделения фронтенда от бэкенда IDE. Потому что тогда не было такой вещи, как бэкендовая IDE. IDE была просто твоим десктопным приложением: испущено — вот оно есть. С тех пор было очень много экспериментов, разных концептов, и eventually это превратилось во Fleet.
[09:29] Александр: Я тебя как раз пытался спросить, что было в начале, что тебя задрайвило изначальным толчком. И ты ответил: вот эта идея, что можно версионировать код и в каждой версии жить как будто это отдельный снэпшот, слепок.
[09:48] Андрей: Не совсем конкретно это, а скорее концепция облачной IDE. Ну вот как если бы был Google Docs, но про код.
[09:54] Александр: То есть разделение программы на клиент и бэкенд. Сейчас это кажется простым — ну да, бэкенд, клиент, придумали протокол общения и погнали. А тогда IDE была данностью: просто программа. Ну вот Photoshop возьмём — программа для компьютера, и всё.
[10:09] Андрей: Это был год 16-й или 17-й, когда я начал этим заниматься. И идея разделения как раз ложится в первые два требования к новому продукту — remote development и коллаборативная разработка — прям как влитая. Это уже рационализация той идеи: у неё есть такие бизнес-применения, как ты правильно сказал. Но изначально была идея вывернуть IDE наизнанку, раздробить её на более-менее задекаплённые запчасти. Она меня драйвила именно на уровне идей.
[10:47] Александр: Получается, ты начал какие-то эксперименты — по своей инициативе, один или с группой энтузиастов, — которые могли стать proof of concept: доказать, что разделение на фронтенд и бэкенд в таком продукте, как редактор кода, — реальная работающая штука. Давай, пока мы отсюда не ушли, обсудим эту задачу. Если бы передо мной как перед инженером встала подобная задача, я бы, наверное, воспользовался подходом «а как другие делают?»: идёшь, смотришь, вдохновляешься, делаешь так же или чуть получше. Но тогда, кажется, подобных продуктов, чтобы посмотреть на протокол, ещё не было. По-моему, тогда ещё не было и Language Server Protocol.
[11:37] Андрей: Сложно вспомнить, был ли уже LSP. Возможно, и был, но он не является протоколом для общения с ремоутным бэкендом — он скорее про общение с ремоутным language intelligence. А тут надо придумать всё остальное: с файловой системой как-то поговорить, что-то уметь запускать на этой ремоутной машине и так далее. LSP — это подмножество. Я бы смотрел на то, как работает LSP, чтобы вдохновиться на решение какой-то части проблемы.
[12:09] Александр: Но в целом ты придумывал всё это из своей головы: сам дизайнил, как компоненты должны общаться, как их задекаплить, какой протокол — синхронный или асинхронный, как запускать команды, как получать состояние. Давай, наверное, можем пропустить этап первых кривеньких, косеньких штук — вот когда ты уже понял, что эта архитектура в целом та, которая тебе нравится.
[12:38] Андрей: Честно говоря, я ещё до конца этого и не сделал. Но есть части, про которые прям чувствуешь: вот так оно и должно работать, это инженерно правильно и лучше в целом. В некоторых аспектах идеал был найден, но не полностью. И, говоря про ранние эксперименты, был ещё один момент, который со мной лично произошёл: пытаясь написать фронтенд к IDE, который работал бы в браузере, я открыл для себя вообще разработку фронтенда. Я и до этого занимался фронтендом в Absource, но то был не совсем настоящий фронтенд, потому что Absource был написан на Google Web Toolkit — это такая Java, которая компилируется в JavaScript, со Swing-подобным API для работы с DOM. Это скорее принесение десктопного опыта в браузер. И вот я стал думать: окей, на чём писать фронтенд в браузере, какие вообще технологии есть? Тогда выбор был не очень большой. Я услышал хорошие вещи про ClojureScript и решил попробовать. В комплекте с ним шёл набор фреймворков, принятых тогда для написания фронтендов. Одним был React — точнее, ClojureScript-binding к React. А второй — re-frame; это то, что в React-мире называлось бы Redux.
[13:58] Андрей: Бэкендеры любят программировать базы данных — так почему бы и на фронтенде не программировать против базы данных? За Redux и re-frame стоит одна простая идея: мы храним всё состояние системы в одной-единственной мапе и мутабельно осуществляем transition стейта, применяя к нему некоторую чистую функцию. Получаем новый стейт, публикуем его. После этого React смотрит на изменения стейта и перевызывает какие-то компоненты. А компоненты написаны как более-менее чистые функции, которые читают данные из базы и продюсят UI в декларативной форме. Это сильно отличалось от условного FRP, который был тогда популярен, — я сам написал какой-то FRP-like-фреймворк для Absource. Functional Reactive Programming — идея в том, что мы оперируем не значениями, а какими-то монадами от значения: observable, RX, signal, — по-разному их называют. На них есть стандартная алгебра, ты можешь их композировать, получать новые observable, а в конце в одном месте подписываешься и слушаешь изменения, а из другого места пропагандируешь изменения, записывая в изначальный observable. Pipeline вычисляет derived-данные и продюсит то, что пригодно для отрисовки.
[15:26] Андрей: Концепция React сильно от этого отличалась, потому что здесь не было никакой монады. Ты просто читал state и эмитил UI. И это было очень liberating. Идея, что у нас есть база данных — одна-единственная мапа, в которой лежит всё состояние, — и что мы строим UI по этой мапе, а на саму мапу действуем отдельно, независимым образом, — она с нами и сейчас. Fleet ровно так и устроен. Data flow в такой архитектуре очень прямой: из данных вывели UI, на UI повесили callbacks; когда юзер куда-то кликает, callback вызывается, и в этот момент мы просто выписываем транзакцию в очередь, которая должна поменять базу. База меняется — и начинается новый цикл. Просто один поток данных по кругу. Это сильно отличалось и от того, как написана IDEA, и от того, как вообще были написаны desktop-приложения тогда. Это был просто fresh — я офигел от того, как на самом деле можно писать UI.
[16:30] Александр: Получается, сейчас UI во Fleet работает примерно по такой же логике, как React плюс Redux — концептуально. Но когда ты говоришь про браузер и когда мы говорим про Fleet, мы всё-таки подразумеваем разные истории: Fleet — это клиент, программа, которую я ставлю через Toolbox, и она не в браузере работает. А браузер, про который ты говоришь, — это отдельная штука, на которой ты проводил эксперименты, или часть существующего Fleet?
[17:00] Андрей: Это просто то, в чём я жил. Я думал, что это будет IDE в браузере. Со временем концепция сильно изменилась. Сейчас это десктопное приложение, всё верно, но история идёт по кругу, и здесь есть разные эксперименты. Сколько ни пытались, никак нет редактора кода в браузере, которым действительно пользовались бы люди. Есть VS Code: с тех пор как Microsoft купил GitHub, появилась одна из самых удобных интеграций — можно нажать на точку в репозитории, и у тебя открывается код. Но мне не зашла эта фича: посмотрел — вау, прикольно, но пользоваться не буду. Мне проще сделать git-checkout или checkout PR и открыть локально: жопа не отвалится. Но что-то в эту сторону движется. И VS Code как раз продемонстрировал, что путь к браузеру лежит через десктоп. Если бы ты пользовался VS Code фулл-тайм на десктопе, обычным приложением, то, увидев тот же VS Code в браузере в контексте, где это имеет смысл, ты бы совершенно не испугался — и это был бы бенефит VS Code. Примерно такая же история и здесь: мы из браузера перекочевали обратно на десктоп, но дорогу себе обратно пока не закрыли.
[18:18] Александр: Хорошо. Смотри, к истории про то, как рендерится UI, мы очень верхнеуровнево пришли от вопроса, который я задал: а что в архитектурных решениях тебе нравится по сей день? Получается, такой подход к UI — когда есть некоторый стейт, к нему привязаны чистые функции, и по изменению стейта эти функции перерисовывают какую-то часть экрана — тебе и сейчас кажется удачным решением? И если бы ты делал с нуля какое-нибудь десктопное приложение, ты бы выбрал эту модель?
[18:55] Андрей: Да, конечно. Эта идея сильно продвинула UI-строение и просто перевернула стол. Она иллюстрируется примерами Jetpack Compose на Android, SwiftUI на iOS и macOS. Не знаю, что там сейчас у Microsoft — там всё время что-то новое. Эта идея постепенно из веба перекочевала уже и в мобильные приложения.
[19:19] Александр: Да, я тоже, когда впервые немножко понял, как работает React, как делать в браузере, — в какой-то момент просто щёлкнуло, и ты начинаешь фигачить очень сложные вещи довольно просто. А когда представляешь, как бы ты это делал, если бы деревом надо было управлять вручную, императивно, через OOP-интерфейс, — вот в этот момент ты грустишь.
[19:42] Андрей: Да, я бы такое не сделал, сам UI выглядел бы по-другому. А главное — это даже не про статическую историю, «есть UI, надо его как-то напрограммировать». Это про динамику: про то, как требования к UI меняются во времени, когда надо теперь сделать иначе. Вот это изменение кода под новые требования — самое страшное, что происходило со старыми UI-фреймворками, где надо было руками модифицировать дерево. Обычно это превращалось в кошмар для разработчиков. С React’ом этого не происходит.
[20:15] Александр: То есть ты говоришь: продукту, написанному на старых технологиях, условно пять лет; ты пришёл, это твой второй проект, ты не сильно опытный, кодовая база большая, и надо внести изменение — поменять форму какой-то кнопки, то, как она выпадает, или как меняет вид при наведении мышки. В кодовой базе, написанной в реактивном стиле, это сделать сильно проще, чем в императивной, потому что там может отвалиться вообще фиг знает что в моменте: код не decoupled, он сильнее связан. А в истории с React у тебя просто есть функции, они как-то композируются: одна отвечает за это, другая за то, и оно неявно, но собирается в UI, который ты видишь. Ты меняешь по сути одну функцию — и конкретно эта вещь меняется, а всё остальное работает как работало.
[21:12] Андрей: И именно когда видишь такие бенефиты от подобного программирования, начинаешь проникаться функциональным программированием. Оно превращается в что-то, что тебя по-настоящему освобождает. Я пришёл к функциональному программированию не от Haskell и заумных систем типов, а от очень простого факта: если всё immutable, то это тебя очень сильно освобождает. Именно это заставляет тебя спать по ночам, а не типы и монады. Не в этом главное. 70 или 90% value, которое приносит функциональное программирование, — не там. Оно в чистых функциях и персистентных структурах данных. И эта школа мысли, это осознание очень сильно задрайвило всё остальное.
[21:55] Александр: Fleet — у тебя подкаст про базы данных и редакторы кода, а Fleet это одновременно и база данных, и редактор кода. Больше того, это не просто база данных, а мутабельная база данных: всё состояние Fleet — это одно значение. Ты такой бангер выдал в первой части подкаста, я хотел к этому подвести со всех сторон. Видно, что тебя эта идея действительно вдохновляет — и меня тоже. Я, наверное, её в название как-то вынесу. Смотри, давай немножко отмотаем, потому что ты много интересного сказал про UI во Fleet — давай закончим это, это действительно то, что там сделано хорошо. Я пользовался Fleet, когда его только заанонсили в 2022 году, а перед нашим подкастом всё обновил, посидел, попрограммировал. Что первое бросается в глаза — это плавность UI: то, как он работает, как ты печатаешь код и как он скроллится, как происходят вообще все моушены в UI. На подсознательном уровне это отличается от всего, что я среди редакторов встречал до этого. Такой отрисовки, естественно, нет в Neovim, в котором я программирую довольно много, и нет в IntelliJ IDEA. Когда переключаешься между этими двумя редакторами, ты прям видишь. Это как экраны у Apple начали выходить по 120 FPS: смотришь теперь на старые — и в голове возникает дискомфорт. Такой же дискомфорт возникает после продолжительного взаимодействия со Fleet, когда садишься за другие редакторы. Он тебе как бы говорит: а можно вот так плавно, чего ты даже и не знал.
[23:37] Андрей: Это очень круто слышать, честно говоря. Потому что это ещё одно направление, в которое было вложено максимальное количество усилий — меня и других инженеров. Это борьба за FPS, за плавность анимаций и вообще за отзывчивость UI. Было прям трудно, и в это было вложено много мыслей.
[23:57] Александр: Идея функционального кода здесь, мне кажется, не напрямую следствие. Всё-таки плавность UI — какая её основная идея, какие основные факторы?
[24:06] Андрей: Самый большой фактор, мне кажется, в том, что это было изначальной целью — это был architecture goal. Про это думали с самого начала, поэтому не пришли к состоянию, когда всё тормозит и ты просто фиксишь бутылочные горлышки, чтобы хоть как-то работало. Идея плавности была заложена концептуально.
[24:26] Александр: А если взять именно эту плавность, этот отзывчивый приятный UI, — какая основная архитектурная мысль заложена, что оно так работает?
[24:37] Андрей: Я бы сказал, что это инкрементальность. Главная идея — ни в коем случае не сделать больше, чем нужно. При каком-то изменении модели мы должны сделать минимальное количество действий. Мы как бы пытаемся исследовать speed of light: каким образом можно максимально быстро прийти к новому состоянию UI. Кастомный UI-фреймворк, сделанный нами, постоянно преследует эту цель. Он родился с нуля, вдохновляясь только React’ом, потому что тогда больше ничего не было. И, как оказалось, он практически брат-близнец Jetpack Compose — основан на тех же идеях. В нём есть несколько идей, которых нет в Jetpack Compose, — наверное, не буду сейчас их перечислять. И есть идея computational avoidance: нам нужно по максимуму постараться не позвать юзерский код. Это не бесплатно — за этим идёт какой-то overhead на рантайме, потому что иногда легче позвать код, чем думать, как бы его не позвать. Но если тормозит движок, это можно заадресить в одном-единственном месте, это проблема нескольких людей. А если начинает тормозить юзер-код, эту проблему решить нельзя. Так скейлится разработка: когда много разработчиков эмитят много кода про UI, лучше этот код обходить стороной, потому что не знаешь, кто его написал, при каких обстоятельствах и под какие требования. Поэтому главная идея — computational avoidance.
[25:59] Александр: А юзер-код в редакторе кода — это что конкретно?
[26:01] Андрей: Это всё. Любой код. Fleet состоит из плагинов, есть платформа, которая эти плагины запускает, и платформа должна изо всех сил объехать этот код, чтобы его не позвать.
[26:13] Александр: А какой я, например, могу написать плагин? Допустим, не в паблик-API, а если у меня есть исходники Fleet и я понял, как это делать без документации, — какую-нибудь свою тему могу написать?
[26:27] Андрей: Темы даже заанонсены. У нас есть программа плагинов, это сдокументировано, и можно выкладывать новые темы. Вообще Fleet — это набор плагинов, поэтому всё, что видишь во Fleet, — это плагины. Давай возьмём файловое дерево: сам UI файлового дерева, то, откуда он берёт данные, как оно устроено, — это плагины.
[26:57] Александр: И я могу написать своё файловое дерево и за файлами ходить на какой-нибудь Amazon S3.
[27:04] Андрей: Да.
[27:04] Александр: И это будет медленно. Но показать-то надо как-то. Как Fleet обходит этот вызов или, может быть, делает его один раз? Какое здесь решение?
[27:15] Андрей: Здесь конкретно магии нет — магия происходит на архитектурном уровне. У тебя никогда не возникает даже соблазна пойти в Amazon из рендер-функции, из компонента. Компонент просто что-то рисует. Сама идея unidirectional dataflow предполагает, что данные у тебя уже где-то подготовлены, они локально здесь, — и ты можешь их просто взять из памяти и отрисовать на экране. Это уже не приводит тебя к мысли пойти блокирующим образом в Amazon из отрисовки UI. Не то чтобы ты не можешь так сделать, — но ты так просто не будешь делать. Что ты сделаешь в этот момент — запустишь где-то корутину, которая будет обновлять либо какой-то стейт, либо прямо базу данных. И уже из этого стейта или из базы ты будешь отображать данные в UI. А на основании того, что твоя рендер-функция читает из базы, мы знаем, вызывать её или нет, — потому что мы знаем, что изменилось в базе данных. Вот это большая придумка. Причём когда происходит транзакция, мы знаем не только, что именно изменилось в базе, но и какие компоненты интересовались какими квери к базе. Не то чтобы они запакованы в какую-то монаду observable — нет. Просто компонент, будучи функцией, при своём выполнении интересуется какой-то частью состояния; и если это состояние не менялось, значит, компонент не нужно вызывать. Очень просто. А если произошло пересечение между транзакцией и твоими квери — значит, с большой вероятностью твой компонент изменится, и его можно перевызвать.
[28:50] Александр: Давай я на вопрос, который я задал, попробую сформулировать ответ своими словами. Вопрос был: за счёт чего достигается плавность и отзывчивость UI? Я так это понял: по минимуму вызывать пользовательский код, а всё, что касается отрисовки, делать внутри ядра. И достигается это тем, что архитектура кода и написания плагинов построена так, что у пользователя есть, грубо говоря, функция, которая отображает уже готовый стейт, — и тогда мы можем себе позволить её вызвать, потому что мы по сути возвращаем закешированное, мы не делаем этого похода по сети в S3. И есть какая-то часть, которая всё-таки вызовется, когда по сети сходить надо, но она не будет влиять на этот motion — этот computation выполнится где-то в бэкграунде. А пока он не выполнился — что я увижу в первый раз, когда вызываю? Пустоту?
[29:50] Андрей: Твой компонент вызовется, не найдёт данных, на которые рассчитывал, и, наверное, сам нарисует себе крутилку или что-нибудь ещё.
[29:57] Александр: Так, кажется, мы потихонечку приближаемся к базе данных — раз мы говорили про avoidance каких-то вычислений против неё. Но давай пока про базу данных и транзакции немножко притормози, будем дозированно жесть наваливать. А про функциональное программирование ты очень интересную мысль сказал, которую я специально отложил, потому что понимаю, что обсуждение может занять время. Тебя вдохновляет идея чистых функций и иммутабельности, потому что она в UI себя показывает: ты видишь разницу, когда применяешь этот подход, — что он позволяет с точки зрения организации и развития кода. И ты к этому пришёл не прочитав книжку какого-то чувака, который говорит «монада — это моноид в категории эндофункторов», а зашёл с практической точки зрения: чтобы решить задачу, нужна чистая функция. Мне это очень понравилось, потому что у фпшного мира есть флёр, что они начинают подвязывать математику, и мир ФП как будто состоит по большей части из математиков, нежели из инженеров-практиков. Меня, инженера, это немного отталкивает. Но ты сейчас немножко меняешь моё ощущение этого мира — что с инженерной стороны как раз можно прийти и получить очень много полезного. Есть что добавить?
[31:53] Андрей: Как ещё можно было бы прийти к функциональному программированию и насколько это на самом деле естественно. Когда ты думаешь про тип int в своём языке программирования, у тебя, наверное, не возникает вопроса, мутабельный ли тип int. Вот у тебя есть число 15 — ты же никогда не думаешь, что сегодня оно 15, а завтра 16. Это просто число, оно существует в мире идей как значение, которое означает число 15, и больше ничего. Насколько стало легче программировать, когда строки стали иммутабельными? Это не всегда было так. Если ты программируешь на Java, у тебя строка иммутабельная — у тебя даже не возникает мысли мутировать её in place.
[32:37] Александр: Ну, справедливости ради, StringBuilder иногда всё-таки используем.
[32:39] Андрей: Да, ты используешь его, чтобы запродюсить новую строчку и её опубликовать. Чтобы в моменте что-то помутировать. Но идеи воспринять строку как массив чаров и менять их на месте у того же массива — в голове, наверное, не возникает.
[32:58] Александр: Я из поколения тех, кому уже дали как данность: строки в Java иммутабельны, и всё.
[33:04] Андрей: Да, как-то мы так придумали, что строки должны быть иммутабельны, — и с тех пор они иммутабельны, и ни у кого нет большой боли от этого. И тут возникает вопрос: если мы начинаем делать агрегаты из этих простых значений — из boolean, чисел, строк, — например, структурку или tuple, которая содержит их три, это выглядит естественным. Ты говоришь про значение — строчку «foo» или число 15 — и так же естественно говорить про агрегат из этих значений как про что-то цельное, что ты никогда не будешь мутировать. Просто есть пара из строки «foo» и числа 15. Это можно продолжать и дальше: а почему у нас коллекции мутабельны в языке? Почему строка иммутабельна, будучи в глубине души массивом чаров, а ArrayList, будучи таким же массивом в памяти, почему-то мутабельный, — и это дано из коробки в JDK? Такое простое рассуждение приводит тебя к агрегатам — к значениям, которые состоят из более мелких значений и тоже сохраняют это свойство иммутабельности. Наука изобрела персистентные коллекции: это векторы, мапы и сеты. Они довольно эффективны. Это не значит, что они копируются каждый раз, когда ты добавляешь элемент, — они шарят структуру с предыдущей версией. Ты копируешь просто путь от корневой ноды до какого-то листика. И они удивительно эффективны: если говорить про чтение из хешмапы или из массива, они проигрывают мутабельному хешмапу дай бог в полтора-два раза.
[34:44] Андрей: Такая композируемость значений находит отражение в композируемости функций. Если функция принимает значение и возвращает значение, то, вызвав её внутри другой функции, эта внешняя функция обладает тем же свойством. Если ты составил функцию из таких же функций, которые ничего не делают, кроме как принимают и возвращают значение, то и внешняя функция такая же. И оказывается, что из этого можно построить вообще весь мир. На практике для построения большой сложной системы — например, редактора — вполне может быть достаточно одного-единственного атома, одного atomic reference.
[35:18] Александр: Знаешь, что у меня немножко не укладывается в голове? Я прекрасно понимаю, что функциональное программирование, чистые функции и их композиция — наверное, самое лучшее, что подходит для UI. Но в мире баз данных, например, нет такой повестки, как ФП. Никто там про это не говорит, потому что компьютеры устроены так, что регистры мутабельные, кэши мутабельные, кэшлайны мутабельные, оперативная память по адресу тоже мутабельная. И получается, что там этого нет — по крайней мере, когда ты не пользуешься базой данных, а разрабатываешь её, пишешь структуры данных вроде страниц, где что-то может быть удалено на месте и тебе нужно делать дефрагментацию страницы. Ты, наверное, сделаешь новую страницу и поменяешь ссылочку, но это локальная оптимизация. А с каким-нибудь векторным исчислением, когда используешь AVX-инструкции: подгружаешь в регистр специальные значения, ждёшь, пока подгрузятся, вызываешь их, потом из регистра читаешь ответ — у тебя прям императивное программирование as is. Там сложно придумать чистую функцию, потому что эффекты есть. Мне интересно провести параллель: мы же не можем всё на свете программировать функционально — или всё-таки можем, с твоей точки зрения? Если взять весь софт, не только UI.
[36:41] Андрей: Давай пока без баз данных, попробуем подумать над простыми вещами. Например, мы пишем функцию map, которая принимает на вход самый обычный массив — не sequence, не linked list — и функцию, которую хотим применить ко всем элементам, чтобы получить новый массив на выходе. В реализации мы не особо задумываемся: берём массив, аллоцируем себе новый и меняем его in place, заполняя соответствующими значениями. Ни одного функционального программирования в этот момент не пострадало — в том смысле, что функция является чистой: приняла массив, вернула массив, но внутри для оптимизации воспользовалась старой доброй мутабельностью и прямой записью в память in place. Можно ослабить требование персистентности с допущением, что значение ещё не опубликовано. Если ты ещё никому не показал свой список, а только строишь его здесь, то это нормально — мутировать его in place, потому что всё происходит в рамках одной функции, и её свойство не нарушается: она остаётся чистой. А значит, всё, что ты построишь из этой функции, тоже будет чистым.
[37:49] Андрей: Эта идея продолжается не только на массивах. Например, если у нас есть дерево, которое мы меняем функциональным образом — копируя путь от корня до листика, — и хотим сделать много операций, какой-то batch сразу, то логично: если мы это дерево ещё ни разу не публиковали и обрабатываем только элементы нашего batch insert, то, приехав поиском в тот же самый лист, который мы хотим поменять, вообще-то окей поменять его in place, если его больше никто не видит. Если внешний мир его ещё ни разу не наблюдал, мы можем продолжать менять его in place, потому что разницы для внешнего наблюдателя не будет. Это не observable mutability, это мутабельность в рамках одной функции. Это находит своё отражение во всяких линейных типах, в оптимизациях компилятора, которые проворачивают в чисто функциональных языках: статически можно доказать, что значение никто не видел, — и можно продолжать менять его in place. А в userspace это отражается в некоторых API — например, у персистентных коллекций часто есть свойство transient: ты берёшь персистентную коллекцию, действуешь на неё изменениями, которые возвращают тебе новую версию, но с важным дисклеймером, что предыдущая версия невалидна и её нельзя использовать. Мутабельность выражена не в том, что мы действуем на предмет мутабельным образом, а в том, что, подействовав на предыдущий предмет, мы его потеряли — его больше нет. И такими небольшими ухищрениями можно пойти довольно далеко и писать довольно эффективные штуки.
[39:43] Андрей: А эффективность их в том, что мы избегаем в этот момент user-level-локинга. Если у нас есть мутабельная структура данных и она наблюдаема снаружи, то нам не остаётся ничего, кроме как брать лок на неё — и чтобы почитать, и чтобы записать. Если структура персистентная — никакого лока для чтения не нужно. Ты просто берёшь atomic reference, получаешь дерево — читай его хоть весь день, никого это не интересует. Но если ты ещё никому не показывал, можешь продолжать его мутировать. И такие ухищрения часто используются в базах данных: всякие реализации MVCC этим и занимаются. И, отвечая на вопрос про функциональные проявления в базах данных, — там их ещё больше. Кроме транзакционности есть ещё такое наблюдение: если ты говоришь про какое-то значение, то попытка менять его in place означает, что у значения есть какой-то адрес, что оно где-то живёт. Ты его меняешь — и все теперь видят его изменённым. Ты упоминал кэши процессора, регистры, память, диск — вот эту иерархию сторэджей. Так вот, сказать, где хранится данное значение, — не такой тривиальный вопрос: одна версия сейчас загружена в регистре, другая в кэше, третья в памяти, четвёртая на диске. Что означает изменение in place чего-то? Да ничего не означает, некорректный конструкт. Ты можешь поменять конкретный бит в памяти, конкретный регистр, значение локальной переменной — но это всё разные вещи, и построить корректную систему на этом невозможно.
[41:04] Александр: Грубо говоря, если мы берём те же MVCC — Multiversion Concurrency Control, — то в целом можно сказать, что эта идея вполне себе… функциональная.
[41:31] Андрей: Абсолютно.
[41:32] Александр: И я хотел тебя спросить: знаешь ли ты про базу данных, которая называется Datomic?
[41:37] Андрей: Datomic — да, по-моему, это база данных в мире Clojure. В браузере, что ли, она живёт?
[41:41] Александр: Нет-нет-нет. Datomic — это база данных, действительно написанная на Clojure. Её идея в том, что сама база данных является значением. Концепция MVCC возведена в то, что не существует read-транзакций: транзакция — это только про запись. Это распределённая база данных, единственный транзактор занимается записью.
[42:00] Андрей: Ну, сейчас там уже не единственный: есть Datomic Cloud, разные варианты. Но концептуально идея в том, что записи делаются по очереди, а чтение не требует координации с этими записями. И когда ты получаешь базу данных, ты получаешь не connection, не берёшь никакую транзакцию — просто получил значение, сиди с ним. Читай его хоть весь день, это не останавливает время. Такая база обладает свойством бесконечного линейного скейлинга по чтениям, но запись при этом является bottleneck’ом — как она всегда и являлась.
[42:32] Александр: Как раз Datomic меня вдохновлял, когда я думал про базу данных. Это отвечает на вопрос, бывают ли базы данных функциональными. Да, бывают.
[42:41] Андрей: Да, бывают, это правда. Просто нет такой дискуссии о чистоте, ни слова из функционального программирования в мире баз данных — там как будто бы используют другие слова.
[42:52] Александр: По сути, для тех же концепций, просто мы называем их немножко по-другому. Транзакционность та же самая: транзакция валидна, это переход из одного состояния в другое, обновление. И чтобы у всех участников процесса мир не ломался: то, что не опубликовано, никем не видится, а то, что опубликовано в какой-то момент, было опубликовано на 100% и, начиная с этого момента, доступно каждому. Но единственный вопрос — действительно ли это надо называть функциональным стилем, или это просто…
[43:30] Андрей: …это здравый смысл — писать такие системы вот так. Ну, ты можешь аргументировать и в обратную сторону: зачем тебе слово «функциональное программирование», когда есть просто здравый смысл, и он означает именно это? Это вопрос, насколько value добавляется в дискуссии, когда ты начинаешь говорить про персистентные структуры данных и чистые функции, в каком контексте эти слова добавляют ценности.
[43:53] Александр: Ну да, просто LSM-дерево, например, LSM-tree — это же по сути key-value, персистентная структура данных, write-optimized: можно фигачить в него дофига записей, и он будет отлично себя чувствовать, а на чтениях есть некоторый трейд-офф. Но никто никогда не говорит, что это персистентная структура данных, — по крайней мере, я не слышал. Говорят просто «LSM-дерево». Хотя у них есть внутреннее версионирование, есть структура данных в памяти, структура на диске, есть процессор, который асинхронно их подмерживает. Но в целом это key-value.
[44:35] Андрей: Вот здесь как раз функциональное программирование может добавить ценность всей дискуссии про внутреннее устройство базы данных. Проблема в том, что это устройство остаётся внутренним. Предположим, у тебя есть база, которая внутри реализована при помощи LSM-дерева и действительно устроена из соображений здравого смысла, — но если наружу она представляется как какой-то store, в который можно положить значение, а потом прочитать его в следующей строчке, вызвав функцию, которая может поменять базу in place, то в следующей строчке после вызова ты видишь новый state. Вся эта функциональная красота спрятана внутри базы, а пользователь вынужден страдать: страдать оттого, что мир под ногами меняется as you go, страдать от race conditions, от безумно написанного кода. И ценность выворачивания наизнанку в том, чтобы дать leverage этих свойств внутренней персистентности на юзер-левел. Теперь у юзера функция, которая ковыряет базу, является просто функцией от базы данных. За этим есть большой value: я могу не брать никакую транзакцию, а просто взять snapshot базы и читать его, не беспокоясь, что удерживаю какие-то ресурсы на сервере. В Datomic этого не происходит — ты не удерживаешь ресурсы на сервере, когда берёшь snapshot.
[45:56] Александр: Да, это правда. Но всё-таки, когда мы говорим про чтение, про read-heavy-операции, это отлично ложится. А бывают же write-heavy-операции. Heavy не значит only — мы иногда и читать хотим. И вот в контексте записи как та же школа мысли интерпретирует очень нагруженную запись, где прям надо перформанс выжимать?
[46:24] Андрей: Здесь идея LSM-дерева лежит на поверхности и вполне применима. Под действием записи мы поддерживаем в памяти некоторый буфер, имеем его in place, и он растёт; а когда вырастает, мы начинаем замерзать его в деревья дальше вниз по логарифму. Такой live set в архитектуре Datomic может поддерживать себе каждый пир, который является веб-сервером в нашей концепции. Все write-heavy-операции можно просто бродкастить на всех читателей, чтобы они, пытаясь получить новую версию базы, брали какой-то снапшот в этом логе, который сейчас под активной записью. А в какой-то момент, когда транзактор решает замерзить эти данные в persistent storage, он объявляет всем пирам: всё, вы можете бросить свой live set, он больше не актуален, теперь читайте из persistent storage. Так с записью борется тот же Datomic.
[47:19] Александр: А является ли эта запись в Datomic… он в целом может быть распределённым, правильно? Запись происходит из одной точки, потому что они выбирают консистентность?
[47:29] Андрей: Ага. Запись распараллелить по многим машинам мы не можем. Но хорошая новость: машины, которые занимаются записью, занимаются только записью — в чтении они вообще не участвуют.
[47:41] Александр: Вот классическая ситуация. Я записал значение 1 и тут же иду его читать в своём коде. Записал на условно одном воркере, а читаю с другого. У меня есть гарантия, что я увижу своё записанное значение, или нет? Ведь нужен какой-то дилей, чтобы от записи дошло до чтения.
[48:03] Андрей: Когда функцию transact отпустили, есть гарантия, что при получении нового снапшота базы ты увидишь эти данные.
[48:09] Александр: То есть я буду читать новый снапшот базы. По сути, снапшот — это следующий инкремент циферки, версия данных у меня будет другая. И за этой версией чтение пойдёт туда, где оно было записано?
[48:21] Андрей: Нет, чтение не идёт с того места, где ты делал запись. Чтение всегда идёт из двух мест: из live set, который тебе набродкастили, и из общего персистентного стораджа.
[48:32] Александр: А если я ещё не дождался этого бродкаста, сетка моргнула между двумя нодами?
[48:37] Андрей: Тогда ты получишь предыдущий снапшот. Но это, кстати, ещё одна история про моделирование времени, про моделирование реальности. Полезно свыкнуться с мыслью, что наблюдатель всегда смотрит в прошлое. Это происходит и в реальности: когда мы наблюдаем что-нибудь в окружающем мире, мы видим это с дилея, потому что есть latency — фотон летит до глаза, глаз воспринимает с отставанием, потом пока до мозга дойдёт, пока осознаешь. При этом ты не останавливаешь мир от того, чтобы делать свои вещи, — ты просто смотришь за окружающим миром. Наблюдателей очень много, мир как-то меняется, и почему-то его никто не останавливает, чтобы посмотреть, правда? Никто не берёт лок. И это наблюдение полезно для программирования. В какой-то момент нужно отпустить этот мир и сказать: любой наблюдатель, который что-то читает из нашего состояния, всегда смотрит в прошлое. Это всегда так. И в этот момент понятие read-лога становится абсурдным: ты сейчас остановил игру, чтобы посмотреть на мяч. Что ты делаешь?
[49:41] Александр: Да, это очень интересная мысль. Я как раз часто об этом задумывался, но такой дискуссии нигде не слышал. Особенно когда мы говорим про базы данных: транзакции, кто какие изменения увидел, кто не увидел, — это же прям теория относительности, как есть.
[50:04] Андрей: Да, в общем-то.
[50:04] Александр: Моделирование происходящего, что происходит везде одновременно. Естественно, мы не можем взять E = mc², применить — и вот у нас система мира в компьютере. Но действительно, когда ты понимаешь, что, глядя на Солнце, ты видишь его состояние восьмиминутной давности, а сейчас там другое… Что такое «сейчас»?
[50:25] Андрей: Что такое «сейчас», всё верно. Если ты говоришь про чистые функции и значения, то от этого знания можно на секундочку отвлечься. Когда ты написал функцию, которая принимает базу как значение, она уже ничего не знает про «сейчас», не знает, какое значение она смотрит. И для корректности её работы совершенно не принципиально, какой момент времени она наблюдает. Она отвечает на некоторый вопрос: для данных ей на вход значений возвращает новое значение. И все вопросы про время из неё исчезают. В ней больше нет времени. А время остаётся где-то на границе системы — там, где кто-то на самом деле берёт и что-то меняет, какой-то указатель переставляет, называет вот это прошлым, а вот это будущим. И это написано в одном-единственном месте: один-единственный луп, который этим занимается.
[51:14] Александр: Но, возможно, кто-то из слушателей задаст себе вопрос: а как же причины-следствия? Ведь есть причина, есть следствие, и они должны быть упорядочены. Время повернуть вспять мы пока не можем. И тогда, если я обладаю знанием, что я записал что-то в систему, мне сказали «записалось», — я такой: так, вот это причина. Дальше я хочу увидеть следствие: начинаю читать, и, допустим, у меня есть запрос, который агрегирует температуру по всем дням. Я добавил температуру по новому дню, иду в этот запрос: так, теперь это должно поменяться, потому что вот причина, вот следствие. И модель eventual consistency, что я всегда читаю какое-то состояние, здесь немного не работает, потому что я могу увидеть состояние до того, как я записал, — без этого нового дня. Данные туда ещё не долетят.
[52:07] Андрей: Не совсем. Если у нас есть функция transact, которая принимает какие-то данные, должна опубликовать их куда-то, что-то записать, — то когда она тебе вернулась, она возвращает тебе новое значение базы в качестве возвращаемого значения: то значение, которое она записала. И если ты против этой версии базы теперь запустишь свою query, она, конечно же, увидит результат записи. В этой концепции — что ты вызвал функцию, получил значение базы и с этой базой что-то вычисляешь — нигде не сказано, что мы осуществляем коммуникацию с каким-то сервером, стораджем или бог знает чем. Про это ничего не сказано. Если система вообще из одного-единственного процесса, то здесь даже записи на диск могло не произойти. И даже в случае распределённой системы запись на диск не обязательно происходит, потому что функция transact отправляет на транзактор заявку сделать такую транзакцию и дожидается, что транзакция действительно выполнилась. В этот момент мы знаем, что получили всю novelty с тех пор, как эта транзакция выполнилась, включая её. И если мы возьмём базу на этот момент времени, она будет объединением всего, что лежит в сторадже — что там старое, — и куска лога, который мы сейчас записали. Такая пара будет являться новой валидной базой, которая учитывает предыдущую запись.
[53:21] Александр: Да, но, скорее всего, если мы говорим про долговечность, где-то запись на диск всё-таки должна случиться.
[53:25] Андрей: Да, транзактор — конечно, да. Но это мы сейчас Datomic обсуждали. В общем, я тут продаю Datomic.
[53:33] Александр: Хорошо. Идея на самом деле очень простая, но понять её инженерную важность мне, например, потребовалось несколько лет: в начале карьеры я вообще не знал, что это может так просто и эффективно работать. А это возвращаемое значение базы, которое приходит мне на запись, может быть чем угодно — начиная с того, что это просто инкремент эпохи, следующий таймстемп. И дальше я этот таймстемп беру и с ним иду читать, а система понимает, что ей нужно вернуть мне актуальное на этот момент состояние. С помощью одной цифры я, по сути, осуществляю управление причинно-следственной связью. Идея простая, но, когда строишь системы, её очень полезно где-то использовать в своих инженерных практиках: она суперпростая, всегда работает, у тебя монотонно возрастает — ну, даже не монотонно, а просто возрастающее число, — и оно помогает решить кучу проблем.
[54:29] Андрей: Да, всё верно.
[54:34] Андрей: А ситуацию Fleet немножко интереснее делает вот что. Мы только что обсуждали, что состояние лежит в базе данных. Что это означает на практике? Что, например, у тебя открыт какой-то файл, у него есть текст, и этот текст тоже лежит в базе данных как значение. Одно гигантское значение, но всё ещё значение. И когда ты в него печатаешь, ты применяешь транзакцию к базе, которая должна вставить букву в этот текст, получить новый текст; этот новый текст как значение записывается в базу, публикуется новый снапшот, UI начинает отрисовывать — и вот таким макаром мы в редакторе печатаем. Но если мы добавляем рассуждения про коллаборативность и ремоутность, становится немножко интереснее, потому что ты уж точно не хочешь ждать 100 миллисекунд, пока добежишь до сервера, чтобы напечатать букву. И вот здесь мы приходим к идее не одного таймстемпа, а двух. Это то, что есть во Fleet, — идея распределённых оптимистичных транзакций.
[55:22] Андрей: Если говорить про архитектуру Fleet, то у него есть много фронтендов, подключённых к центральной точке, которая называется workspace-сервер; она подключена к разным внешним сервисам, в том числе LSP, IntelliJ IDEA, ReSharper, специальному агенту, написанному на Rust, который мы деплоим куда-то в космос, где файлы лежат на диске. В такой архитектуре workspace хранит какую-то правду о стейте. Он нужен, чтобы держать твоё состояние: все клиенты могут отключиться, уйти, а workspace продолжает жить своей жизнью, потом ты подключаешься и получаешь актуальное состояние данных. Там все буковки, которые ты напечатал, eventually сохранятся. Чтобы печатать буквы консистентно и в свой локальный снапшот базы, и в этот workspace, и при этом чтобы все остальные пользователи тоже консистентно печатали буквы в тот же файл вместе с тобой — чтобы у всех оказалось одинаковое состояние текста, — надо применить два таймстемпа. На этих двух таймстемпах и на персистентности всей вселенной весь Fleet и работает.
[55:56] Андрей: Действие в следующем. Если у меня есть какое-то персистентное значение и у тебя есть такое же значение, они одинаковые. И если я применю к своему значению какую-то функцию, а ты — к своему другую функцию, то мы придём к разным значениям. Но если мы обменяемся этими функциями и договоримся, в каком порядке их применить, то можем выровнять наше состояние. За счёт того, что мы просто вернёмся в прошлое: скажем, если я был первый, а ты не успел, то ты вернёшься в прошлое, применишь сначала мою функцию, а сверху — свою. И эта идея простого механизма по типу git-rebase даёт нам эти самые распределённые оптимистичные транзакции. За счёт того, что наша база персистентная, мы можем сохранить какое-то количество снапшотов в прошлом и после этого переписывать историю.
[57:26] Александр: Давай теперь разберём это чуть подробнее, потому что на этом моменте кому-то может быть непонятно, и я бы добавил немножко визуализации. Я часто переговариваю более простыми словами то, что сказал гость, — и самому что-то понять, и слушателям. Получается, мы говорим о следующей модели: представляем двух программистов, сидящих за разными компьютерами, подключённых к одному серверу, — они вместе разрабатывают какое-то backend-приложение. Чтобы нормально вместе работать, им нужно общее знание глобального стейта: состояние документа и у тебя, и у меня — оно у обоих вот такое. Копия этого документа может лежать у тебя, у меня на компе и ещё на workspace, на бэкенде — на этих трёх машинах. А может лежать только там, на workspace, а мы с тобой вдвоём только подключились, и у нас ещё локально ничего нет. Но главное, что на уровне идеи есть один документ. Потом мы начинаем его менять. Для сложности, конечно, мы меняем одну и ту же строчку — сигнатуру одной и той же функции. Например, эта функция состоит из двух аргументов. Я смотрю на неё, думаю: первый аргумент должен быть другой, — и добавляю новый аргумент в начало, первым параметром будет i: int. И примерно в этот же момент ты сидишь, видишь, что Саша начал печатать, думаешь: он меняет эту функцию, мне тоже надо, — и добавляешь новый аргумент, но в конец, последним. Была функция из двух аргументов: я сделал из трёх, добавив в начало, ты — из трёх, добавив в конец. И вот мы это делали одновременно, и каждое наше нажатие на клавишу — это отдельная транзакция на уровне системы, которую мы посылаем на workspace. В процессе печати ты будешь видеть, как в начале функции возрастают символы, а я — как в конце добавляются одновременно со мной. Строчка была узенькая, а стала длинная, выросла с двух концов. Символы как-то по очереди применились, у тебя и у меня смерджилось в одно состояние, и мы увидели функцию из четырёх аргументов.
[1:00:19] Александр: Но если мы будем делать это в условиях плохо работающего интернета, и я, и ты будем добавлять в начало — прям явный конфликт, не только строки, но и колонки, туда же. Я написал — интернет моргнул, ты написал — интернет моргнул. Вот это отправилось на workspace. И как я узнаю, что ты что-то в этом моменте поменял, а ты — что я поменял, и кто выиграет?
[1:00:48] Андрей: Здесь никто выиграть не должен, здесь должны выиграть все. Я должен увидеть твой тайпинг, ты — мой. Тот факт, что мы стали печатать в одно и то же место, означает только то, что мы оба увидим две запечатанные вещи, но желательно одинаковые. Чтобы результат был одинаковый, есть миллион техник. Есть CRDT — структуры данных, на которых операции сами по себе можно перемножать, и они приводят тебя в правильное место. Есть техника operational transformation, когда мы сами операции, которыми обмениваемся, переписываем так, чтобы они учитывали наши локальные изменения. OT использует условный Google Docs. Мы используем ни то, ни другое — мы используем идею путешествия во времени и ребейза. У них у всех разные трейд-оффы, разные сильные стороны. Сильная сторона ребейза в том, что всё, что мы говорили про печать текста в одно место, колонки, строчки, — этого можно было не говорить. Мы могли просто действовать некоторыми функциями на некоторое значение — и это всё, что нам надо знать, чтобы корректно осуществить ребейз. Вот мы только что говорили, что должны смириться с тем, что наблюдаем прошлое. На практике это означает, что если мы смотрим на прошлое и после решаем как-то подействовать на мир, мы выписываем транзакцию, которая должна встать в очередь и примениться уже к актуальному значению мира — не к тому, которое мы наблюдали, когда думали, что собираемся что-то сделать, а к самой правильной версии базы. Вот это и есть асинхронность: когда ты применяешь транзакцию к чему-то, что ещё не видел, переходишь куда-то во времени.
[1:02:23] Андрей: И оказывается, что если ты в этом мире живёшь и готов на это, то это единственное, что тебе нужно, чтобы сойтись по распределённому сигналу. Потому что если ты готов примениться к будущему — чуть-чуть будущее на себя взять, выписать транзакцию, которая внутри сделает то, что требуется, чтобы букву-то вставить, — то это означает, что ты можешь применить её к любому сигналу и к любой версии этого будущего. Если в новой версии будущее изменилось так, что туда прилетела твоя транзакция, которую я ещё не видел, я свою всё равно могу применить уже поверх твоей. Потому что если транзакция была чистой функцией, я могу позвать её сколько угодно раз на любом состоянии, — значит, у меня есть ребейз. Мне не нужно изобретать CRDT, писать правила трансформации операций, вообще ничего говорить про то значение, на котором мы действуем. Это всё, что нам нужно. Очень долго мне требовалось, чтобы это наблюдение сформулировать и осознать. Простое наблюдение: если ты готов к асинхронности, к этим странным путешествиям во времени и релятивизму, то ты можешь переписать прошлое, перепроиграть свою функцию, потому что она была уже готова примениться к другому стейту. Ты не держал лок на базе, чтобы что-то записать, — ты выписал транзакцию, которая применится в будущем, а значит, можешь применить её к любому другому будущему.
[1:03:44] Александр: Давай тогда на уровень пониже. Что такое транзакция? Вот «выписал транзакцию» — что это технически значит?
[1:03:52] Андрей: Когда мы говорим, что выписали транзакцию, это значит, что мы просто поставили лямбду в очередь. Это функция, которая применяется на самом актуальном снапшоте, может его помутировать и запродюсить новый снапшот. Вот это называется транзакция. Это просто функция, которую мы применяем по очереди к некоторому значению.
[1:04:08] Александр: Ну вот, например, я ставлю каретку в какое-то место и ставлю пробел.
[1:04:16] Андрей: Добавить этот пробел — это одна лямбда, которая применяется к документу и возвращает документ, в котором уже есть пробел.
[1:04:26] Александр: А могут ли эти чистые функции складываться в батчи, чтобы не по одной делать, а группами?
[1:04:36] Андрей: Если ты ничего не говоришь про эти функции дополнительно, то нет — остаётся только применять их по очереди, по одной. Ну, ты можешь их скомпозировать, применить большую функцию — это то, что с функцией можно делать. Но на практике у нас это реализовано немножко интереснее, потому что мы не обмениваемся функциями по интернету и даже не рассчитываем на то, что у нас одинаковый код. Мы вместо этого записываем трейс выполнения нашей транзакции: говорим, какие она примитивные операции на базе совершила на актуальном значении. Записываем такой примитивный байт-код транзакции — что надо сделать с базой в более примитивных терминах. И уже этими рекордингами обмениваемся. Иногда они коммутируются, иногда нет: иногда скажут, что эту транзакцию в таком виде применить нельзя, — и ты как автор должен её перепридумать и отправить новую версию. Для этого ты перезапускаешь свою функцию, которая была у тебя в процессе. Есть отдельная интересная техника про то, как понять, что функцию нельзя применить, что база для принятия решения изменилась. Если я печатал в один документ, а ты в другой, мы отправили сообщения на workspace, — то даже несмотря на то, что ты пришёл первый, а я второй, это не означает, что кому-то надо перепридумывать транзакцию. Они не конфликтуют. Но не потому, что мы печатали в один и тот же документ. Не конфликтуют по той причине, что я не читал то, что ты писал.
[1:06:01] Андрей: Конфликт — это не событие записи в одно место. Вот ещё одно важное наблюдение: конфликт происходит не оттого, что мы пишем в одно и то же место, или переписываем одно и то же значение в памяти, или конфликтуем в одной и той же entity. Конфликт в том, что я для принятия решения поменять эту entity читал что-то, что ты записал. И это создаёт неконсистентность. Остальное решается просто — как-нибудь рассчитайтесь, last write wins, дикий запад. Проблема появляется, когда я принимал решение записать что-то, глядя на… Ну, скажем, я перевожу деньги с аккаунта на аккаунт, как у Клада Попова. Я читаю деньги из одной entity, оттуда убираю, кладу сюда. Конфликт появляется тогда, когда ты делал то же самое, — но не потому, что ты тоже отсюда убирал деньги (это мы можем упорядочить), а потому, что я читал то место, которое ты только что заапдейтил. Вот это приводит к конфликту — как раз нарушение причинно-следственной связи.
[1:07:05] Александр: И с технической точки зрения мне нужно что? Мне нужно как-то пометить в этом изменении состояния, что могло быть причиной у этого состояния. Не на 100%, но что я видел. Я это вижу и кладу эту информацию как часть транзакции и отправляю её. И у другой транзакции тоже есть сет того, что стало причиной и что стало следствием, — два сета. И если следствие одной не пересекается с причиной другой, то это два совершенно параллельных действия, которые никак друг другу не мешают. Собственно, это идея оптимистичных транзакций в базах данных — то, как они работают.
[1:07:49] Андрей: Всё именно так и устроено. Мы делаем хитрое отжимание: в каждое значение, которое храним в базе, аккомпанируем ещё специальным числом. Это число призвано закодировать причину, почему там именно такое значение. Оно является хэшом от всех других таких же чисел, которые мы читали в этой транзакции, чтобы туда что-то записать. Скажем, я читаю из A, что-то с ним делаю и записываю в B. Это означает, что в B я записал не только насчитанное значение, но ещё хэш: значение, которое я прочитал вместе с B, — я прочитал этот int, подмешал к нему специальную соль, рандом, — и кладу теперь в B. И теперь это значение B отражает — мы не можем сказать как, но как-то отражает причину, почему оно там такое записано.
[1:09:01] Александр: Это что, какой-то блокчейн тут уже начинается?
[1:09:01] Андрей: Получается, что да. Когда мы валидируем какую-нибудь транзакцию, которая приходит после этого, она содержит специальный байт-код, примитивную операцию, которая говорит: провалидируй, пожалуйста, вот это значение, что оно всё ещё с таким числом. Мы не отправляем даже значение, которое ожидаем, не считаем никаких хэшей от значений — просто передаём вот это магическое число. Мы ожидаем там увидеть его, что его никто не трогал с тех пор. И если оно такое — транзакцию можно применить, а если нет — надо отклонить.
[1:09:11] Александр: То есть мы говорим не только про набор меток до и после — мы ещё можем отследить цепочку изменений. И если в этой цепочке была хоть одна метка, которая пересекается с нашей хоть одной меткой, то у нас будут разные числа, и мы должны откатить до того момента, где всё ещё было одинаково. А детектить это очень просто — надо просто пересечь сеты интов. Это и будет ответ, конфликтует или нет: правда ли, что два сета интов имеют хотя бы что-то общее.
[1:09:39] Андрей: Да.
[1:09:39] Александр: Получается, что в целом это прям почти блокчейн. Довольно интересно, потому что в базах данных такое есть. Например, в TigerBeetle есть подобное. Но там это больше из соображений безопасности, насколько я понял: чтобы ничего не продолбать вообще ни в коем случае, сверить, что у тебя, у меня и у третьего-четвёртого одинаковое значение, что точно мы. Даже если трое из нас умрут, хоть у кого-то останется. А у вас эта идея — чтобы понять, с разным мы работали или с одинаковым, ответить на один простой вопрос. Или даже принять решение, как функции друг с другом связаны: надо ли их упорядочивать между собой или можно просто смерджить. Короче, ребейз сделать или мердж.
[1:09:53] Андрей: Да, всё верно. Причём мы, конечно, включаем эту информацию с потерей. Мы не собираем все сеты всех причин всего, что было до, — мы берём просто одно число, хэшик хитрый, который отвечает на один тупой вопрос: пересекается или нет. Но нам этого достаточно. Мы придумаем такое число, которое внутри содержит достаточно энтропии и знаний о причинах, почему там именно такое значение, что мы можем их просто сравнить. Это то, как Fleet синхронизирует данные между сервером и всеми клиентами.
[1:10:56] Андрей: И есть ещё одна импликация функционального программирования. Мы говорили, что всё эффективно работает, потому что наша база персистентная: мы можем хранить какое-то количество последних операций, чтобы эффективно делать ребейз, перепроигрывать функции на прошлом, переписывать историю. Второй момент, где это сильно помогает, — мы изначально говорили про требование бороться с фризами. Персистентность даёт не только отсутствие необходимости в read-логе для чтения. Она позволяет запускать целую гору рутин, которые оперируют против этого общего состояния, против базы, никого не блокируя. И поэтому не существует ситуации, что я пошёл что-то посчитать, засчитался, долго считаю — и заблокировал UI от отрисовки следующего кадра или заблокировал тайпинг. Скажем, где-то в бэкграунде вычисляется дифф, который занимает чуть ли не квадратичное время, а я в это время нажимаю букву. И я не хочу, чтобы вычисление диффа помешало мне напечатать эту букву. Это достигается за счёт того, что база персистентная: я как читатель этого стейта не беспокою писателя, не блокирую его. Так мы и боремся с фризами.
[1:12:09] Александр: Слушай, а есть ли ситуации, в которых эта идея выглядит костыликом и надо где-то что-то присесть, чтобы оно заработало? Я думаю, в идеале сам код — то, что мы видим, документ, вот это значение — окей, но от этого состояния могут запускаться другие процессы, которые тоже потом надо будет откатывать, если что-то пошло не так. И этот откат насколько болезненный?
[1:12:41] Андрей: Их надо не откатывать, их надо перезапускать — перезапускать на новом входе. Я читал какую-то функцию на документе, например считал его дифф с гитовой головой, пока он у меня в процессе находится. Занимался этим какое-то время. Если с тех пор документ поменялся, у меня есть два варианта. Первый: выкинуть свои вычисления и перезапустить заново при попытке принести их в базу. Я выписываю транзакцию, которая идёт и проверяет, что там всё на месте: если на месте — запишу, если нет — вернусь и заново запущу. Второй вариант: не выкидывать, а посмотреть, что случилось с тех пор, и применить к моему посчитанному значению все операции, которые с тех пор произошли. В случае диффа это работает: я могу посчитать хороший дифф, а потом довольно эффективно применить к нему операции, случившиеся с тех пор, чтобы он был примерно корректен на новое состояние документа, — просто растянуть ренжи. То же самое происходит с хайлайтингом. Бэкенд считает хайлайтинг, приносит мне информацию: тут ошибка, тут ворнинг. Эта информация посчиталась на какой-то момент времени, а с тех пор я уже что-то натайпал. Мы, конечно, запустим новый хайлайтинг и будем его ждать, но этот-то мы не будем выбрасывать в форточку — мы просто применим его к существующему тексту, аккуратно подправив на те изменения, что случились с тех пор. Примерно так всё во Fleet и работает. Это контрибьютит в плавность, но у этого есть свои даунсайды и проблемы, с которыми приходится бороться.
[1:14:11] Александр: Слушай, наверное, такой закрывающий вопрос про распределённое состояние оптимистичных транзакций. А что конкретно является critical path? Что происходит с ним во всём этом? Ведь где-то что-то происходит не асинхронно — выполнение какого-то стрима идёт, оно главное. Что там минимально необходимое и достаточное, чтобы он двигался вперёд? Что за суть лежит в этом critical path?
[1:14:39] Андрей: Не уверен, что понял вопрос, но могу ответить, где bottleneck в этой системе. Bottleneck — просто в той очереди, которая применяет функции к актуальному состоянию базы. В текущей реализации эти функции применяются по очереди, эффективно одна транзакция за раз. И так как в транзакции выполняется произвольный юзерский код, мы можем замедлить всю систему, просто написав медленную транзакцию. Хорошая новость: это всегда понятно, как фиксить. Просто слезь с транзактора, посчитай всё заранее, а потом попробуй применить это на очереди. Второй способ фиксить — написать асинхронные транзакции, которые можно считать в сторонке с прицелом на то, чтобы перепроиграть их, когда конфликт случится. Мы можем такую же машинерию провернуть локально: раз у меня есть функция, которая что-то читает и что-то пишет, я могу запустить её отдельно, посмотреть, что она читала и что писала, а потом уже сесть на транзактор, на эту критическую очередь, которая всё держит, и попробовать применить мои операции. Если нельзя — вернуться и попробовать снова. Самая частая проблема в этой системе всё ещё вот этот единственный транзактор, луп, который применяет функции по очереди.
[1:15:56] Александр: Этот луп, который применяет их по очереди, — это и есть тот основной стрим происходящего, как я выразился. В этом лупе не происходит рендеринга — он происходит в другом месте. Как раз поэтому я могу бесконечно ресайзить окно, пока происходит что-то другое. Но в этом лупе уже есть транзакция — она уже сформирована, и в ней уже есть сет того, что она видела, сет того, что она меняет. Я пытаюсь понять подсчёт вот этого хитрого числа.
[1:16:27] Андрей: В определённой ситуации — если это транзакция, которая пришла ко мне из интернета, от тебя или от workspace, — она приходит уже в том виде, который мы можем применить. Она исполненная, у неё есть вход-выход. Результат её применения — либо да, она подходит, либо нет, иди перепридумай. А когда мы говорим про транзакции, которые я выписываю сам — тайпинг документа делаю, — это обычная функция, которая выполняется в этой же очереди, просто функция, написанная от руки.
[1:16:52] Александр: Как происходит подсчёт того, что в этой функции следствие, а что причина, — вход-выход? На вход ей идёт документ, вот этот весь документ, — это то, что я видел? Или всё-таки какие-то строчки?
[1:17:08] Андрей: То, что ты читал внутри документа. Ну, текст — это целиковое значение, одно-единственное, поэтому, даже если мы с тобой печатаем в разные места одного документа, это потребует переотправки транзакции. Это часть даунсайда того, что метод настолько дженерик — он уже не расщепляет текст на более мелкие участки. Такими вещами не занимаюсь, потому что на практике это не нужно.
[1:17:32] Александр: И в целом, я так понимаю, этот хитрый хэш считается тоже довольно быстро?
[1:17:36] Андрей: Да, потому что это хэш от таких же хэшей. В этом хэше не участвует само значение.
[1:17:40] Александр: А в первом хэше?
[1:17:42] Андрей: А там тоже хэш того, почему там именно такое значение. Они считаются от рандомов: мы берём, придумываем рандомное число и подмешиваем его во все значения, которые пишем. А потом там, где читаем, берём это значение и такие: а, мы посчитали вот это, а ещё подмешали рандома сверху. Всё это вместе захэшировали и записали в то место, куда сейчас собираемся писать.
[1:18:05] Александр: Вы оптимизировали блокчейн тем, что не заставили майнеров подбирать количество нулей в этих хэшах, а берёте первый попавшийся.
[1:18:14] Андрей: Ну да.
[1:18:16] Александр: Круто. Мне прям очень эта идея понравилась. А как ты к ней пришёл? Это же неочевидная идея.
[1:18:22] Андрей: Довольно просто. Когда мы говорили с самого начала про React, re-frame, то, что мы применяем функции, а база данных — это одна-единственная персистентная хешмапа в памяти, — оказалось, что я на практике столкнулся с race condition. По наивности своей молодой я написал очень просто: применяю любые операции, которые мне приходят из интернета. Но потом оказалось, что я применил их здесь в таком порядке, а там в другом, — и получилась какая-то ерунда. Естественный способ это фиксить, если у тебя уже есть персистентное значение и ты уже применяешь чистые функции, — ты ничего, кроме этого, не придумаешь: просто переупорядочить функции, переписать историю. Эта идея супер на поверхности. Если о ней говорить прям с порога — «мы во Fleet переписываем историю, чтобы что-то где-то слиплось», — это звучит сложно, звучит как что-то, что сложно придумать. Но это зависит от отправной точки. Смотря откуда ты начнёшь решать проблему, придумаешь разные решения. Если ты решаешь функционально — у тебя все значения и все чистые функции, — ты придумываешь одно решение. Если начинаешь думать с другого места — придумываешь operational transformation. С третьего места — придумываешь CRDT. Это зависит от того, с какой стороны ты идёшь.
[1:19:49] Андрей: И вот здесь мой опыт программирования на Clojure — то, что я могу рекомендовать любому человеку. Это настолько liberating и так сильно меняет то, как ты думаешь о проблемах и дизайнишь системы, что стоит это сделать даже не для того, чтобы писать на Clojure профессионально, а просто чтобы расширить свои инженерные границы. То, что в реальности обычно является сложными проблемами, на Clojure чаще всего даже не становится проблемой. Вопросы сериализации обычно не стоят: если всё, чем ты оперируешь, — это обычные данные, персистентные значения, векторы и мапы, в которых лежат числа, булианы и что-то попроще, то вопросы всякой сериализации внезапно отпадают сами собой. Потому что ты всегда программируешь внутри процесса так, как если бы программировал в распределённой системе. Нет разницы. Разные другие вопросы такого сорта просто переформулируются.
[1:20:46] Александр: У меня был опыт программирования на Scala, но он всё-таки не такой чистый, как на Clojure. Всё-таки Clojure — это прям функциональное программирование, а Scala — что-то не от мира сего. Про транзакции мы, мне кажется, поговорили супер подробно — прям разобрали, как это происходит и работает, и мне даже стало понятно. Мы поговорили про то, как отрисовывается UI: что он не зависит от этих транзакций, а применяется к стейту, который меняется этими транзакциями, — в целом это React-подход. Про функциональное программирование проговорили довольно много. Можно, наверное, перейти немножко к Fleet как к редактору. Обсуждали высокие концепции, а в названии подкаста всё-таки будет Fleet. И тут ребята пришли — блин, про редактор кода говорят, два с половиной часа ни слова про редактор, какое-то функциональное программирование. Что это вообще такое? Давай как про продукт — про то, что обычные люди будут использовать.
[1:21:56] Андрей: Ну, Fleet — это редактор кода попроще. Что-то попроще, если сравнивать с IntelliJ-семейством, и даже, может быть, попроще, чем VS Code. У этой простоты две составляющих. С одной стороны, скоуп проекта настолько огромен, что даже помыслить о том, что мы можем достичь фич-паритета с IDEA, абсурдно. С другой стороны, мы и хотели чего-то попроще: не было цели повторить IDEA или даже к ней приблизиться. Была цель написать редактор, который будет унифицированно, одинаково плохо работать со всеми языками. В смысле — быть полиглотным, быть меньше про проекты, про настройки, про то, чтобы тебе сильно-сильно помогать, а про то, чтобы давать какой-то минимум, с которым ты можешь работать на всех языках, — быть ближе к VS Code в этом смысле. Ближе к редактору, а не к IDE. Здесь есть философская составляющая: IDEA пытается быть такой мамочкой, у которой в ней всё. Пользуешься Tomcat — есть конфигурация, которая Tomcat запустит. Пользуешься Docker — обязательно есть плагин, который нарисует докер-контейнеры и скажет, как их запускать. А Fleet этого ничего не делает и пытается оставаться простым, не включать в себя ничего, что не является inherently частью текстового редактора. У этого есть и практическая сторона (мы не сможем всё сделать), и продуктовая (мы пытаемся быть полегче).
[1:23:30] Александр: Да, в этом плане всё логично, хотя с одной стороны можно подумать по-другому. IntelliJ IDEA — большой продукт, один из немногих, который живёт уже 20 лет, развивается и не стагнирует с точки зрения пользователя: оптимизируется, живёт. Но мы, как программисты, понимаем, что код имеет свойство устаревать, и «переписать всё нафиг» хочется всегда — примерно сразу после первого коммита. И вот взяли ребята, решили переписать нормально, учесть опыт 20 лет. А ведь и так всё нормально, IDEA продаётся. Зачем? Зачем рисковать? Как ты людям объяснишь, что надо перестать пользоваться IDEA, пересесть?
[1:24:26] Андрей: А у нас нет такой цели, мы даже не пытаемся это объяснить, не пытаемся это говорить. Это что-то другое. Если ты посматриваешь в сторону VS Code — посмотри сначала в неё. Большим риском было бы ничего не делать. Большим риском было бы со всеми имеющимися ресурсами просто строить весь бизнес вокруг IntelliJ IDEA как платформы. Это попытка сделать что-то ещё, что-то сбоку, что не будет менять жизнь нашему большому количеству пользователей, а будет привлекать новых. У нас совершенно нет идеи, что мы всех пользователей IntelliJ перетащим на Fleet. Скорее адресуем новых пользователей и тех, кто по каким-то причинам недоволен.
[1:25:12] Александр: Какие, например, причины могут быть у пользователей IDEA? Возможно, есть что-то, что ты не хотел бы называть, но какими крупными мазками — что людям может не понравиться в IDEA?
[1:25:24] Андрей: То, о чём мы говорили с самого начала, — полиглотность, желание иметь один простой инструмент вместо нескольких посложнее, более универсальный. Эта концепция, которая пошла, наверное, ещё от какого-нибудь Delphi: там была среда, где вот Pascal, и ты просто что-то открыл в программе. Потом Visual Studio для C# — большая, которая тебе всё делает, и ты только на C# и, по-моему, на плюсах. Xcode такой же: берёшь Swift, вот тебе кнопочка «сделать». А скрипт в Xcode писать не очень хочется. Та же история в IntelliJ IDEA с Java. Вот как раз идея, что инструмент, даже работая в разных проектах с разными языками — не в одном проекте много языков, а просто два проекта, один на Go, другой на Rust, третий на Java, — мне нужно три программы, три окна, три процесса. Везде надо ещё обновления поддерживать. Где-то я keymap настроил, где-то не настроил; переключился, промахнулся, бешусь. А хочется, чтобы это была одна программа, которую ты настроил, и минимально необходимые вещи — навигация, git и что-то супер-универсальное для всех — ты уже знаешь на кончиках пальцев. А всё языко-специфичное, проекто-специфичное, фреймворк-специфичное — если есть, то окей, а если нет, то это не останавливает тебя от того, чтобы начать работать.
[1:26:36] Андрей: Как тебе, кстати, экспириенс с Fleet, если ты пробовал для Java или Kotlin?
[1:26:44] Александр: Я пробовал, давай в конце — есть мне что сказать. Я как раз из тех, кто любит один инструмент, а не много, потому что люблю оптимизировать его прям очень сильно. Я ярый пользователь Neovim. Не настолько ярый, чтобы писать в Neovim на Java, — я провёл столько времени в Emacs, столько бесконечных ночей, чтобы его настроить, это целая история. Первая версия Fleet, условно того, что называлось Fleet, была написана на Clojure, и по этой причине я использовал Emacs, потому что для Clojure тебе не нужна никакая IDE. Это была целая история: у меня есть git-репозиторий с моим конфигом, а там лежит Lisp — куча скобок, которые ты скопил. Никто никогда это не прочитает и не поймёт, но в git это должно лежать. Ты раскатываешь себя, всё время что-нибудь подкручиваешь, находишь новые пакеты в интернете, пробуешь новые колор-схемы, новый лэйаут, новые способы искать по проекту. Целая история, целое хобби.
[1:28:44] Александр: Это действительно как брендирование профессиональной жизни во что-то самобытное. Странно говорить, что емаксеры эффективнее тех, кто работает в IntelliJ IDEA, — это сложно померить, и, скорее всего, даже неправда. Мне Emacs не зашёл, я пробовал, но у меня майндсет не емаксовский, и я на Lisp не программирую — вот что самое хреновое. Мне некомфортно было на Lisp программировать, а там надо уметь, чтобы реально что-то под себя делать. Я выбрал Neovim. И это как раз функциональный подход к императивному, потому что в Neovim это Lua — чисто императивная хрень: взял, настроил сверху вниз, прочитал скрипт, понял. Он такой же кастомизируемый, но всё-таки не операционная система, как Emacs, а редактор кода с системой плагинизации, который не всё тебе позволяет, но всё, что позволяет, — понятно и классно. Мне очень нравится Neovim, за исключением того, что он всё-таки в терминале и использует терминальные тулзы для отображения текста.
[1:29:56] Андрей: Да Emacs тоже такой же.
[1:29:58] Александр: Ну, нет, есть же у Emacs standalone-приложения, которые можно открыть.
[1:30:02] Андрей: Весь Emacs при этом всё равно вполне себе уверен, что он рендерится на терминале из 80-го года.
[1:30:05] Александр: Ну да, но не весь — там можно картинку нарисовать. Давай про мой опыт с Fleet отложим до конца, мне есть что сказать, так что слушатели, не отключайтесь.
[1:30:22] Александр: По поводу позиционирования. Мы начинали: Fleet — такой простой редактор, чтобы взял и всё сделал. Очевидный конкурент — VS Code. А что тогда меня как потенциального пользователя VS Code должно не устраивать? В VS Code сделано так, во Fleet вот так, поэтому тебе предпочтительнее.
[1:30:46] Андрей: Ну ты прям самое больное. Если не брать фичи — сколько фич и ресурсов вложил Microsoft в VS Code, это мы оставляем за скобками, потому что это нереального размера ресурсы. Но с точки зрения концепции: анимации у нас плавные, жесть. Приходите за анимациями. Думаю, в некоторых аспектах мы уже можем потягаться по рендер-перформансу. По старт-перформансу мы пока хуже. Можем потягаться по UX — по тому, как в целом построен весь UX, он немножко отличается. В качестве языкового движка используются мозги IDEA. Поэтому если вы замечаете разницу между Java в VS Code и Java в IDEA, а вы её очевидно замечаете, то во Fleet она будет ближе к IDEA. Мы разговариваем с IDEA. И с Kotlin то же самое — там разница ещё сильнее. Я не знаю, как у VS Code с C#, но вся мощь ReSharper с нами. То есть мы оставляем наши сильные стороны — поддержку языка, рефакторинги, знания про код, — но добавляем их в более минималистичный интерфейс. Поэтому если ты привык получать фичи от IDEA, но хочешь более лёгкий редактор, — look no further.
[1:32:10] Андрей: Это с точки зрения бизнеса и питча пользователям. Я-то не продукт, я инженер, и мне нравится идея. Она очень элегантная. Наверное, единственно правильное — оставить мозги IDEA и ReSharper как бэкенд, потому что без них было бы совсем тяжело конкурировать. А тут хотя бы есть накопленный опыт и интеллектуальный ресурс, который есть у JetBrains и который она может предложить как альтернативу. Мы ещё думаем немножко в сторону того, как можно переформатировать использование всего этого кода из IDEA — может быть, использовать его эффективнее, чем сейчас, чтобы стать более лёгкими. Такие эксперименты у нас идут.
[1:33:00] Александр: Мы уже много обсудили. Я тебе обязательно после этой темы предоставлю возможность рассказать всё, что ты хотел бы, что осталось недосказанным. Но мне очень интересно, как работает история с рендерингом. Fleet написан на Kotlin, правильно? Там есть свой Jetpack Compose-а-ля-фреймворк, но полностью параллельно написанный другими людьми, я так думаю. За счёт чего? Там используется нативный рендеринг, то есть нет Swing, как в Java, через который происходит рендеринг?
[1:33:35] Андрей: Java2D-стек, да, мы его не используем — используем Skia и Skija.
[1:33:38] Александр: И как это вообще работает? Про редакторы мы говорим много, а вот как работает именно нативный рендеринг — честно говоря, я вообще не знаю.
[1:33:57] Андрей: Да я тут сам джун. UI-фреймворк мы, конечно, написали и задизайнили, но то, как на самом деле на железе рисуется, всегда было закрыто соответствующими библиотеками. Это была Skia, был WebRender — движок для рендеринга, который используется в Firefox. Это всегда был какой-то чёрный ящик, который умеет круто рендерить и предоставляет наружу интерфейс что-то вроде Canvas, на котором ты можешь рисовать стандартные примитивы, но ещё можешь делегироваться к шейдерам. Шейдеры — это программы, которые исполняются на GPU и могут одной маленькой командой, скажем, здесь что-нибудь, градиент, чтобы моргало ещё. И одним таким вызовом шейдера ты говоришь: применением некоторой функции на GPU мы получим картинку. Это абсолютно профанское представление об этих вещах, я ни одного шейдера в жизни не написал, так что не держите меня.
[1:34:58] Андрей: Но как я понимаю историю с рендерингом: в чём отличие? Раньше мы на CPU растеризовали картинку — превращали её просто в битмап, который можем нарисовать на экране. Теперь мы можем использовать для этого GPU. Для этого берём специальный буфер, записываем в него так называемые draw-call’ы — вызовы шейдеров и примитивные команды. Во время обновления экрана мы не пиксели на экран выбрасываем, а записываем эти командочки, которые надо выполнить, а потом отправляем всё одним махом на GPU. Отправить на GPU занимает время, а выполнение этого на GPU занимает почти никакое время. И за счёт этого сильно изменилась парадигма перерисовки UI. Если раньше в Swing (там есть Repaint Manager) нам надо было запариваться, чтобы перерисовывать только тот участок экрана, который изменился, инвалидировался, — скажем, нажали буковку, и вокруг неё что-то изменилось, надо только это место перерисовать (в IDEA мы аккуратно, фигурно вырезаем, что надо перерисовать; растеризация происходит, отправляется, там тоже CPU как-то используется), — то у нас в этом нет смысла. Точнее, смысл есть, но немножко по-другому. Мы можем просто взять и отрисовать сразу весь экран. И вот так Fleet рисуется — каждый раз заново весь. Весь экран перерисовывается заново, как в игре: просто рисуй всё заново.
[1:36:29] Александр: Да, всё заново.
[1:36:29] Андрей: Промежуточные данные мы, конечно, кешируем — которые надо отрисовать: компоненты, деревья, куда какие лямбды заполнили. Но во время каждого кадра мы сначала заново запускаем весь этот рендеринг, который строит список команд. Потом отправляем этот список одним махом на GPU и получаем очень быструю картинку. Соответственно, bottleneck теперь — это запись этих наборов. Надо минимизировать количество draw-call’ов и время на их запись.
[1:36:59] Александр: Получается, чтобы отрендерить экран, это один вызов к GPU или несколько?
[1:37:05] Андрей: Мало вызовов к GPU. Ты просто записал набор своих команд.
[1:37:08] Александр: А сколько он длится?
[1:37:10] Андрей: Давай так, я сделаю разбивку. У нас есть 120 FPS. Это означает, что у нас порядка 8 миллисекунд на всё про всё, чтобы успеть: и compute сделать, и транзакцию применить, и отрендерить, и всё-всё-всё. Нажали букву — на всё про всё 8 миллисекунд. Правда, для печати буквы требования чуть пониже — latency можно сделать чуть побольше, потому что ты не заметишь. А, например, если скроллишь, то от ивента скролла до кадра надо, чтобы прошло что-то порядка 8 миллисекунд. Опять же, можно с задержкой, но это будет влиять на ватность интерфейса — ты будешь чувствовать себя чуть-чуть… А чтобы оно плавно выглядело, тебе нужно точно уложиться в 8 миллисекунд. Что в это время входит? Нам надо запустить наш UI-фреймворк, чтобы он перевызвал те юзерские компоненты, которые могли инвалидироваться. Это занимает миллисекунды две, иногда четыре. После этого у нас есть время отрисовать сцену в специальный буфер. Этот буфер нужен для double- и triple-буферинга, чтобы UI был более плавным, — мы платим немножко отставанием за большую плавность, чтобы в такт успевать с v-sync. Потом предыдущий буфер должен по-настоящему отрисоваться на экран: мы отправляем набор draw-call’ов на GPU — это, наверное, ещё миллисекунду занимает, может, две. Итого мы примерно borderline. Поскроллить успеваем нормально. Но на всё про всё в userspace у тебя что-то порядка… я в последний раз считал, не помню цифру, но из этих восьми твои на самом деле примерно пять. Остальное — всякая требуха: нативный композитор начинает работать в операционной системе, что-то ещё, пока до тебя событие про скролл доедет. Так что у тебя примерно четыре-пять миллисекунд, чтобы сделать всё.
[1:39:03] Александр: Ну вот прям слон в комнате — а garbage collector?
[1:39:04] Андрей: Garbage collector — история довольно больная. Когда-то мы убежали из браузера сломя голову, потому что, во-первых, перестали верить, что можем в браузере родиться и там остаться (путь к браузеру лежит через десктоп), а во-вторых, убежали к нормальному GC, потому что у V8 тогда с GC была печальная история. Из-за того, что наш код весь функциональный, он постоянно аллоцирует. Memory pressure, GC pressure у нас огромный: ты вызвал функцию, она возвращает тебе значение — где она его возьмёт? Она его аллоцирует, чаще всего. Ты аллоцируешь, как дышишь, когда занимаешься функциональным программированием. И если этих аллокаций прям много, это создаёт давление на GC, и он случается чаще. Если аллоцируешь совсем много — GC случается ещё и дольше. Нужно повнимательнее следить, чтобы иногда что-нибудь сэкономить на аллокациях. Иногда это влияет на плавность UI — думаешь: что случилось? А, это GC случился.
[1:40:11] Александр: Вот бы тут какой-нибудь язык программирования без garbage collector.
[1:40:13] Андрей: Не поможет. Потому что в языках без GC стоимость коллекшена переносится на malloc и free. В языках типа Rust или C++ приходится это переносить. А если мы продолжаем писать в функциональном стиле и относиться к значениям как к значениям, а не как к месту в памяти, которое сейчас будем тыкать in place, то оказывается: если у нас есть GC, то мы можем во время GC переносить объекты в памяти, тем самым делая компакшн. И поэтому можем не слишком париться за аллокации: во время аллокации просто бампаем указатель вперёд. Взяли себе буфер в памяти, сказали, что из него будем аллоцировать; аллоцировать объект — это просто бамп-бамп-бамп: увеличиваешь число на размер, сколько тебе надо. Это бесплатно. Во время GC мы там — поколения, бла-бла-бла, длинная история — эти объекты можем подвигать и скомпактить в одно место, где они выжили, чтобы остальное освободить снова для аллокации. А в языке без GC чаще всего указатели никак не обновляются. Поэтому, аллоцируя объект, надо очень сильно подумать, где его аллоцировать: если неаккуратно, упрёшься во фрагментацию хипа. Поэтому есть free-list’ы, и вот эта операция malloc несчастная занимает очень много времени. А ещё дольше занимает free. Поэтому если мы переносим управление памятью на момент, когда объект больше не нужен, и пытаемся его удалить и аллоцировать в правильном месте, — эти malloc и free начинают стрелять по перформансу сильнее, чем стрелял бы GC.
[1:41:59] Андрей: Представим простую ситуацию: мы аллоцируем что-то на стеке, и тут же при возврате нам вернули значение, мы на него посмотрели, тут же выбросили — больше не надо. Такой паттерн простого вычисления. Мы находимся на стеке, но просим в куче аллоцировать, выходим из стека, снова просим в куче аллоцировать. Такой ворклоуд — malloc/free дорогие. А если эквивалентный ворклоуд на языке с GC — это просто аллокация в молодом поколении и молодой GC. Мы знаем, что есть гипотеза, которая иногда не работает, но если работает — большая часть объектов умирает молодыми. Это означает, что мы просто пробежались по массиву и всё вайпнули нафиг один раз в конце. И оказывается, не так уж очевидно, использовать ли язык без garbage collector. Если ты занимаешься функциональным программированием и действительно много аллоцируешь, то GC — это реквизит не только чтобы считать циклы и разбираться с референсами, а именно для перформанса.
[1:43:02] Александр: Да, ты правильно сказал. Получается, для функционального стиля, где аллокациями ты дышишь, всё время их создаёшь, их такое большое количество, что мы не можем себе позволить так часто вызывать malloc. И, соответственно, нам придётся писать какой-то свой хитрый аллокатор, который будет в буфере, уже зааллоцированном операционной системой, что-то двигать. По сути, свой мини-GC делать только там.
[1:43:32] Андрей: Да. Какую-нибудь арену ты в этот момент используешь, конечно, или в мире Objective-C всякие autorelease pool — это все твои друзья: временные объекты аллоцируешь в специальном месте.
[1:43:45] Александр: Насколько я понял, даже при текущем состоянии, попользовавшись Fleet, я не могу сказать, что GC там проблема для пользователя. Где-то, наверное, какие-то свайки иногда могут быть, но в целом, я думаю, при должном усилии головастых инженеров это всё решаемо.
[1:44:09] Андрей: Ну, это решается обычно довольно банальными вещами. Обычно тот код, который много аллоцирует, — это тот же код, который медленно работает. Это интересное наблюдение. Он медленно работает не потому, что аллоцирует (аллокация бесплатная), но по моему опыту профилирования таких приложений есть приём: можно включить сбор allocation-профайлов, смотреть, где аллокации происходят. И эта информация полезна, потому что ты сразу видишь, что много-много аллокаций происходит в том же месте, которое можно улучшить. И улучшением перформанса этого места — просто если он будет меньше делать работу, он будет меньше аллоцировать. Логика такая: мы аллоцируем как дышим, поэтому там, где мы много аллоцируем, мы дышим много, — значит, в этом месте мы делаем слишком много работы, которую могли бы не делать. Поэтому прямая оптимизация этого места уменьшает не только время его работы, но и аллокации, и тем самым давление на GC. Вся работа про улучшение перформанса одновременно улучшает и ситуацию с GC. Ну и в целом код можно переписать так, чтобы он делал меньше аллокаций. В какой-то момент ты просто об этом не думаешь, фигачишь в цикле new ArrayList, а потом понимаешь, что можно и за циклом его сделать. Такие банальные штуки.
[1:45:23] Александр: Просто раньше, по-моему, когда GC были не такие развитые и делали заметный stop-the-world, была проблема распределённых систем на Java: они просто heartbeat друг другу не могли послать, думали, что нода умерла, а там просто GC-пауза. Сейчас-то GC развились так, что почти никогда не фризят.
[1:45:43] Андрей: Наука GC прошла далеко вперёд, это прям сильно заметно, да, это правда.
[1:45:47] Александр: А какой GC дефолтный, котлиновский? Какой там используется, даже не знаю, — просто используется, и всё, даже не тюнится?
[1:45:55] Андрей: Никак не тюнится.
[1:45:57] Александр: Я просто помню раньше, когда ещё только вот это слово Shenandoah появлялось, и я такой: так, тюнить GC, вот эти ключи постоянно нужны, — ой, господи. А сейчас я забыл, когда в последний раз туда смотрел. Он просто работает, и всё.
[1:46:11] Андрей: Работает — не трожь.
[1:46:13] Александр: Замечательное время же. Для себя я вообще всё, про что мне было бы интересно поговорить, — ты более чем рассказал. Я предоставляю слово тебе: если хотел поделиться чем-то интересным про Fleet или вообще про инжиниринг со слушателями — пожалуйста, делись. Я поддержу любую тему.
[1:46:35] Андрей: Не так, чтобы что-то ко мне пришло прям сейчас в голову. Может быть, через секунду ещё придёт, но я хотел спросить тебя про твои впечатления от Fleet за это время.
[1:46:44] Александр: Впечатление следующее: положительная вот эта плавность отрисовки. Первое, что бросилось в глаза, и потом, чем больше я сидел за Fleet, тем больше понимал, блин, как хреново отрисовывается всё остальное. Остальное — это Neovim и IntelliJ IDEA, две мои программы. Это прям даже, возможно, эффект вау. Я прям оценил с инженерной точки зрения, что ребята над этим постарались — это круто сделано. И главное — теперь не растерять это преимущество с развитием продукта. Мне бы хотелось пользоваться Fleet через несколько лет, и если он пойдёт по развитию, которое со мной внутренне совпадёт, я бы хотел, чтобы плавность была такая же. Лучше не надо, мне кажется. Опять же, это первый опыт того, что намного проще и умещается в голове. Никита Прокопов на предыдущем выпуске сказал фразу: у него Sublime умещается в голове. И я понял, что это довольно комфортное ощущение для инженера — понимать в целом всё. Каждый уголок недосконально, но представить себе картинку: если мне нужно сделать это — нажимаю туда, оказываюсь там; сконфигурирую здесь — получаю вот это. Нет вот этой магии и монструозности, которая всегда была и есть с IDEA: ты нифига не понимаешь, как она работает, какие там механизмы. Инженеру некомфортно работать с программой, которую он не понимает. У нас с IDEA такая дистанция: я её использую просто потому, что ничего лучше для Java нет, а всё остальное пытаюсь делать в любимом Neovim.
[1:48:30] Александр: Поэтому мне понравилось, что ты просто открываешь меню во Fleet. Я по всему прошёлся, всё прочитал, всё понял сразу. Из того, что можно скастомизировать, там плоская структура, нет глубоких вложенностей табов. Я даже поиском не пользовался, чтобы что-то найти, — просто открываю меню и глазами понимаю, куда идти. Это круто. Очень понравился JSON-конфиг, за это отдельный респект. Потому что я вообще не знаю, как IDEA конфигурируется: что там, XML какой-то где-то валяется, он проекту принадлежит, лежит в папочке .idea или где-то в инсталляции, или где-то у меня в аккаунте JetBrains. Мне это не нравится, не нравится, что за меня кто-то что-то решает. А тут говорят: вот тебе настройки. Интуитивно понимаешь, что настройки почти один к одному мапятся на JSON, и этот JSON просто лежит. При желании я могу пойти в него, поменять, закоммитить в git — и всё. Мне понравился этот подход. Например, в Neovim похожая история: у тебя просто есть конфиги, ты хранишь их в git, правишь. А здесь ещё UI на этих конфигах накручен, но прозрачно накручен.
[1:49:42] Андрей: Да, он прозрачно накручен, автоматически рисуется из того JSON. И, добавив в JSON-схему новую хрень, я её автоматически уберу.
[1:49:54] Александр: Окей, это даже ещё лучше. Чего не хватает? Сейчас я откладываю один момент, на самом деле он единственный. Мне понравилось именно как печатать. Это же не просто плавный скроллинг — ты именно печатаешь, и есть такое качественное внутреннее ощущение. Когда с какого-нибудь лагающего Xiaomi перешёл на iPhone — просто всё по-другому рисуется, и это приятно. Какая-то элитарность отрисовки есть. Когда печатаешь, это тоже комфортно, прям хочется печатать. Конкретно по UI-ным штукам мне понравилась идея, что табы эти по краям — внизу, справа — я могу очень гибко убирать. Понимаю, как их приклеить к какой-то стороне, как убрать, как они живут. Табы в IDEA — у меня целый стрим есть на YouTube, где я эти табы убираю и пытаюсь найти меню, где их убрать и как они называются. А тут кнопочки есть, просто нажал — окей. Организация визуального пространства тоже хорошо продумана. Я бы вообще всё нафиг убрал, но понимаю, что я довольно специфичный пользователь. Возможность убрать всё нафиг самому меня устраивает.
[1:50:57] Александр: Почему я попрограммировал во Fleet примерно минут пять и дальше не смог? Предлагаю тебе угадать. Шорткаты не как в IDEA.
[1:51:13] Андрей: Не как в IDEA. У меня в IDEA используется плагин, который называется IdeaVim.
[1:51:19] Александр: А, IdeaVim. А ты пробовал Vim во Fleet?
[1:51:23] Андрей: Вот это exactly то, что я хотел сказать. Я попробовал, увидел эту галочку — она там есть, Vim Mode, и даже довольно близко, — но Vim Mode пока сыроват.
[1:51:37] Александр: Не работает вообще.
[1:51:40] Андрей: История такая, что это тот же самый код, который используется в IDEA для Vim — вот этот IdeaVim, только немножко переколхоженный, чтобы заработал во Fleet. Я так думаю, что в IDEA есть ряд либо костылей, либо ещё не втащили во Fleet, что очень сильно портит пользовательский опыт именно Vim-взаимодействия.
[1:52:00] Александр: Что именно? Нет command-mode, то есть двоеточие и дальше.
[1:52:05] Андрей: Там двоеточие — он, по-моему, начинает печатать двоеточие.
[1:52:11] Александр: Я привык писать :w, это сохранить файл. И да, в IDEA он нихрена не делает, я окей, но у меня на уровне рефлексов, что-то написав три недели назад, привычка писать :w приводила к тому, что я печатал w в коде, а он должен просто сохранить файл. Мне окей, если он не сохраняется, но эта мышечная привычка после печати нажать :w осталась. И представляешь: я привык печатать, а тут делаю :w, и я напечатал левую фигню, и она не компилируется. Сейчас этого нет — и это респект. Надо будет ещё раз перепробовать, потому что я обновился до последней версии.
[1:52:58] Андрей: А пробовал включить такую штуку — называется Caret Smooth Animation True? Нужно зайти в Settings, Actions, Edit Settings.
[1:53:09] Александр: В общем, я включил такую настройку, чтобы каретка, видимо, Smooth меняла своё положение не линейно, а как-то нелинейно. И это выглядит как в каких-то играх, когда двигаешь прицелом, камерой. Такой прикольный эффект.
[1:53:28] Андрей: Надо попробовать. По поводу Vim: раз уж действительно я сейчас могу писать код, — я прям не мог писать код на той версии релиза. А где настройка собственно этого? Может быть, это вопрос документации. Как мне импортнуть мой IdeaVim-конфиг сюда, чтобы все мои биндинги…
[1:53:49] Александр: У IdeaVim есть кастомный конфиг?
[1:53:51] Андрей: Да.
[1:53:51] Александр: В Vim есть такое понятие, как leader key. Это специальная кнопка, которая входит в такой мод, где дальше она ждёт от тебя нажатия двух клавиш — и это будет команда. У меня leader key — это пробел в normal-моде. Я нажимаю пробел (под большим пальцем, очень удобно), и дальше две кнопки: например, ff — это Find File; я запускаю экшен в IdeaVim Find File. Пробел fc — Find Class. И таких последовательностей у меня довольно много, и они в файлике .ideavimrc. Я могу спросить у ребят и тебе передать. Им надо добавить это в документацию обязательно, потому что это все вимеры — у них есть эти файлики. И эти последовательности, эти клавиши настолько в корку въедаются, что ты даже не думаешь об этом. Там go to definition в каком-нибудь LSP, рефакторинги простенькие тоже на этих последовательностях. И когда их нет — ты просто не можешь программировать, идёшь обратно в Neovim.
[1:54:51] Андрей: Посмотри в документации; если там не написано, я могу сказать ребятам.
[1:54:57] Александр: Да, я посмотрю после этого. В общем, если убрать этот момент — я, наверное, попал на нестабильный релиз с Vim, а сейчас command-mode есть, всё работает. И если я смогу его так же сконфигурировать, чтобы работал как в IDEA — вот этот файлик, — то могу сказать, что не вижу сейчас никаких препятствий писать код фулл-тайм во Fleet и на Java, и на всём, потому что поддержка Java — это как раз то, что мне нужно. Наверное, сделаю какой-нибудь апдейт, в Telegram-канале напишу. Я вижу в этом идею и смысл. Но на Rust всё равно буду в Neovim писать.
[1:55:37] Андрей: Но видишь, тут уже удобнее, когда ты стираешь границу настроек: когда настройки лежат в файлике .vimrc и в системе у тебя Vim, то по сути программы разные, но ведут они себя одинаково. И поэтому довольно удобно переключаться.
[1:55:55] Александр: Понятненько. Спасибо вам большое за Fleet. Вопрос, наверное, ещё такой. Я видел у вас на главном сайте — с точки зрения документации, продвижения, маркетинга, позиционирования. Мы, кстати, с Никитой обсуждали вопрос, почему у Zed получилось так выстрелить именно в маркетинге. Может быть, у тебя есть мысли, что надо сделать, чтобы про Fleet все начали говорить и на уровне Zed про него жужжать?
[1:56:27] Андрей: Знал бы — уже жужжали бы, да? Я тебя понимаю, я же тоже инженер, в такие материи про хороший маркетинг совсем не умею. Знаешь, какая основная проблема? Мы делаем инженерный продукт для других инженеров, и ты должен как-то его продать, но ты не умеешь продавать. Однако люди, которые продавать умеют, вообще ничего не знают про инженерию. И найти пересечение этих людей, которые могут и продать, и написать, и понимают вообще всё, — это прям уникальные люди. И, наверное, какой-то такой человек, или два, есть в Zed — поэтому это работает. Мне кажется, это секрет. Когда ты смотришь и видишь людей, которые продают инженерный продукт, но не пишут код каждый день и не писали его пять лет до этого и вообще не особо горят тем, про что рассказывают, — нет химии, ты такой: ну, очередной, пошли дальше.
[1:57:27] Александр: То есть это ощущается?
[1:57:29] Андрей: Ощущается, да. Именно на уровне внутреннего восприятия.
[1:57:35] Александр: А в Zed ты читаешь и такой: блин, чувак, который программирует, понимает, что говорит. Я Zed установил, там Vim работает, Motion, и просто офигенно, потому что у них целая баллада про то, что они чуть ли не Vim-first — основной разработчик вимер, — и я прям сразу залетел. Мне понравилось, как всё сделано. Но тоже в целом для задач, которые решает Zed, у меня есть Neovim. А вот для тех задач, которые Neovim решить не может и Zed не может, а Fleet может, потому что он может использовать мозги IntelliJ IDEA…
[1:58:06] Андрей: А у Neovim для Rust что, rust-analyzer используется?
[1:58:10] Александр: Да, rust-analyzer.
[1:58:10] Андрей: А во Fleet тоже rust-analyzer по дефолту?
[1:58:16] Александр: Большинство растаманов пишут в Neovim, там комьюнити просто сформировано: берёшь плагинчик, стянул с GitHub настройки, к себе применил — и тоже так делаешь. Такая движуха. Neovim — номер один по популярности среди растаманов, по крайней мере, то, что я вижу. Я, правда, на Rust пишу только для себя на стримах, но подмечаю, что все, кого я смотрю, тоже это делают в Neovim. И, возможно, как раз эта аудитория может быть потенциальной для Fleet. Как ей продать — конечно, другой вопрос. Может быть, через Kotlin попробовать, тех, кто пишет на Kotlin? Постримить что-нибудь.
[1:58:54] Андрей: Проблема в том, что есть много стримов с Kotlin, но на них нет просмотров. Я вру: есть прикольные стримеры, прям стримеры, и когда они используют Kotlin — всё супер. Но когда ты стримишь Kotlin, чтобы продать Kotlin, это немножко не работает. Опять же, люди чувствуют это на уровне, не объяснить.
[1:59:16] Александр: Если я, например, начну — вот я сейчас на Rust стримлю, базу данных пишу, — очевидно, что мне никто ничего не занёс, мне просто это нравится, глаза горят, я шучу, это весело. А когда кто-то стримит «сейчас будем разрабатывать вот это, смотрите в IDEA» — оп, и скипнул сразу. Но, думаю, через какую-то комьюнити-движуху можно. Почему я завёл эту тему? Планируется ли open-source-ить что-то из кода Fleet, например ту же UI-библиотеку?
[1:59:45] Андрей: Сложный вопрос. Он активно обсуждается. Про подробности лучше смотреть у нас на сайте.
[2:00:05] Александр: Хочется, чтобы был open source?
[2:00:07] Андрей: Мне было бы интересно посмотреть, как можно писать десктопный UI на Kotlin, потому что у меня есть интерес писать UI. Я умею писать на Kotlin, и это для меня как для бэкендера такой вопрос: а можно писать UI-чик на Kotlin, какой-то для себя, может быть, программку или мини-продукт? Потому что если я сейчас хочу писать десктоп на чём-то — я бэкенд-инженер, вариантов немного: Electron, ну, Electron что-то не хочется, не горит глаз.
[2:00:39] Александр: Jetpack Compose?
[2:00:41] Андрей: Да, наверное. Но мне кажется, что Jetpack Compose — это для людей, которые Android разрабатывают, и им прям супер изи.
[2:00:47] Александр: Нет, оно на самом деле изи всем. Просто, может быть, когда я на него смотрел, он был сыроват, надо обновить.
[2:00:54] Андрей: А что на Jetpack Compose написано, кроме того, что я знаю точно, — JetBrains Toolbox? Он активно сейчас заезжает в IDEA, ну и во Fleet мы тоже потихонечку смотрим в сторону Compose. По разным причинам про это думаем. Одна — чтобы залевэриджить комьюнити, чтобы они писали плагины к нам: если это знакомый Compose, всё хорошо. Вторая — просто не хочется поддерживать второй UI-фреймворк.
[2:01:34] Александр: Тогда получается, чтобы мне ещё хотелось… У вас там база данных какая-то своя самописная?
[2:01:37] Андрей: Да-да-да. Она уже open source, кстати.
[2:01:41] Александр: Она уже open source?
[2:01:41] Андрей: Она лежит на GitHub в IntelliJ Community Edition. Там есть папочка Fleet, и там есть какие-то запчасти, которые мы уже за-open-source-или, потому что они теперь ещё и в IDE используются. Наши технологии немножко заехали уже в IntelliJ. У нас монорепо — то есть много репозиториев, но есть один самый большой, это IntelliJ. В нём лежит папочка Community — та папка Community, которую ты видишь на GitHub. В этом же репозитории лежит Fleet, и есть кусок Fleet, который используется в IDE Community, и он лежит в Community. Частично он уже open source.
[2:02:15] Александр: Да, я понял. Окей. Что ты отмечаешь для себя, или что бы хотел сказать слушателям подкаста — тем, которые хотят развиваться в инженерном плане, которые горят, у которых глаза, но, возможно, им нужно немножко подсветить нужные части в мире разработки, в карьерном и профессиональном росте? Что бы ты отметил?
[2:02:44] Андрей: Я так и думал, что такой вопрос будет, но почему-то сразу про это забыл и совершенно не подготовил ответ. Мне кажется, функциональное программирование и особенно программирование на Clojure — как на функциональном языке, в котором нет всякой шелухи: нет типов, нет никакой математики, нет тех сложностей, которые обычно идут вместе с функциональным программированием, — этот опыт сильно на меня повлиял, сильно изменил то, как я смотрю на проблемы. Я его могу только рекомендовать: даже если вы не будете программировать на Clojure за деньги, стоит написать на ней хотя бы какую-нибудь нетривиальную систему. Не веб-сервер, который реализует HTTP API, а что-нибудь более или менее сложное — какой-нибудь игровой движок или просто игру. Это может очень сильно изменить перспективу. Почувствовать себя сильным, что ты можешь делать много.
[2:03:37] Андрей: Один из моих опытов был уже довольно поздно — когда я уже программирую Fleet, мы его пишем на Kotlin. Не так давно у меня какое-то время был хобби-проект: я писал компилятор своего языка программирования. Писал его на Clojure, и получился компилятор, который таргетит LLVM, поддерживает лямбды, функции, структурки, числа, циклы, ифы — всё, что ты знаешь. Правда, я не дошёл до интересных вещей, но компилятор тривиального языка занимает 350 строчек. Это возможность ухватить только самую суть программирования, забыв про ритуалы. Это даёт эмоциональный подъём и чувство уверенности, что ты на самом деле любую проблему можешь решить, — потому что ты можешь думать о ней проще, чем до этого. Проблемы становятся проще, если о них хорошо подумать и если у тебя есть для этого правильные перспективы.
[2:03:57] Александр: Интересно.
[2:03:57] Андрей: По поводу дизайна систем, архитектуры — текущее моё соображение в том, чтобы попробовать рассказывать историю про систему, которую разрабатываешь. И эта история должна быть связной, короткой и понятной. Если ты не можешь рассказать мне одну историю про то, как система написана, английскими словами, просто по-человечески, то что-то не так. Стоит концептуализировать любую проблему и больше писать. Больше над любой проблемой работаешь — пишешь: вот моя проблема, сегодня я её решаю, вот так я её понимаю. Что-нибудь дебагаю, какой-нибудь баг ищу — можно записывать свои шаги. То есть как можно больше писать. Писать помогает, помогает более ясно думать.
[2:04:58] Александр: Да, я полностью поддерживаю и про «писать», и про суть программирования, и про то, что стоит почувствовать, что ты можешь сделать что-то действительно большое. Потому что сути, логики и мысли, которую ты закладываешь в программу, обычно не так уж и много. А больше — это ритуалы, с которыми ты борешься на пути к тому, чтобы эту мысль выразить. По факту ты как прослойка между клавиатурой и стулом должен как раз эту мысль формулировать и выражать, а всё остальное должно делаться как-то вокруг. Потому что зачем тебе сидеть и два часа дебажить, не знаю, undefined behavior?
[2:05:54] Андрей: Это хорошо, если два часа.
[2:05:55] Александр: Ну и так далее. То есть действительно ценность мозговой активности головы ты начинаешь более чётко осознавать. И понимать, что таки не заменят машины людей талантливых.
[2:06:12] Андрей: Пока да.
[2:06:13] Александр: Спасибо большое за такой внятный и развёрнутый ответ. Людям, которые захотели прямо сейчас пойти и сделать что-то на Clojure, куда им пойти?
[2:06:24] Андрей: Не знаю, я давно там был. В интернет? Да, выйдите в интернет. Просто clojure.org — там всё написано, есть и language guide, и что делать, и документация. Вроде бы там последний раз, много лет назад, когда я это делал, всё было понятно.
[2:06:39] Александр: Хорошо. Ну что, спасибо тебе, Андрей, большое.
[2:06:43] Андрей: Да, дисклеймер: опинии мои, как обычно, я не от лица компании. Всё прочее, если вдруг это важно.
[2:06:51] Александр: Ну, я могу в начале это ставить.
[2:06:53] Андрей: Я рекомендую использовать Clojure, я не рекомендую это от лица JetBrains. От лица JetBrains я, конечно, рекомендую посмотреть на Kotlin. Нормальный язык, кстати. На нём получается программировать большую программу на много человек, не оказавшись в ООП-supe что ли. Попробуй программировать так, как если поубирать некоторые фичи языка, — получается нормальный язык. В смысле, на нём можно писать церковный код, он будет довольно sane.
[2:07:15] Александр: Это очень точное слово — «если поубирать некоторые фичи языка». Это то, что хочется сделать с Kotlin. Знаешь, в некоторых языках хочется что-то добавить, а вот с Kotlin хочется что-то убрать.
[2:07:27] Андрей: Да. И больше ничего не добавлять. Просто убрать половину, и будет классно.
[2:07:33] Александр: Вот видишь, мне в этом он и не импонирует. Потому что когда есть куча возможностей сделать одно и то же, куча разных способов, стилей, и даже вопрос не в языковых конструкциях, а вот…
[2:07:44] Андрей: Выбери одну. Ну, ты понимаешь, ты потом должен на протяжении всей жизни проекта всем объяснять, что вот этот выбор имеет под собой смысл.
[2:07:52] Александр: Да. И потом новый человек придёт — он будет плеваться. Короче, это ненужные сложности, их не должно быть.
[2:08:01] Андрей: Это такой трейд-офф в языке. Иначе у тебя получится Go. Очень opinionated язык: там написано, как надо делать, но с тех пор сильно передумали, как надо на самом деле, а Go остался таким, как был.
[2:08:15] Александр: Ну, видимо, это у всех языков программирования. Я вот сейчас программирую на Kotlin, мне не нравится, что null-safety по факту не так уж и решена. Стектрейсы и NPE как бы остались, они есть.
[2:08:29] Андрей: Ну, на границе интеропа с Java, да?
[2:08:31] Александр: Как бы нет, но я-то пишу код и вижу, что они есть. Как это их нет? Null может быть вот здесь. Потом меня напрягает, что extension-функции окей, если ничего другого не использовать, только их юзать. Но когда что-то откуда-то пришло, я не понимаю откуда, и оно здесь как бы без IDE, — я вообще не могу прочитать код, потому что мне нужно понять, откуда эта extension-функция. А, она из той библиотеки, блин. Всё, я понял. И вот этот момент — я такой, блин, имплиситы в Scala, вот это они, и это плохо было в Scala. Ну и наследование companion object — это всё, не нужно уже давно, всё надо поубирать.
[2:09:15] Андрей: Так и есть.
[2:09:17] Александр: Ну, может быть, какой-то Kotlin 2.0? Не K2, а вот переосмысленный?
[2:09:21] Андрей: Нет.
[2:09:23] Александр: Нет?
[2:09:23] Андрей: Такого не будет. Ломать язык никто не собирается.