Это не риторический вопрос. Я знаю, так исторически сложилось, но... почему мы продолжаем использовать текстовые файлы?
Вы можете сказать, что это очень простое решение: что видишь, то оно и есть. Но мы имеем дело с несколькими слоями абстракции, а все абстракции текут. Это приводит к ошибкам, запутанным диффам и проблемам с производительностью.
Давайте углубимся в искусство хранения кода в виде текстовых файлов.
Текст
Компьютеры оперируют бинарными данными, поэтому для представления текста им приходится использовать кодировки. Существует много вариантов: UTF-8 или UTF-16; с BOM или без него; CR, LF или CRLF; little-endian или big-endian — это лишь самые популярные варианты. К сожалению, мир не смог договориться об одном стандарте, поэтому то и дело приходится сталкиваться с проблемами: испорченные диффы, ошибки в тулзах, проблемы с копированием, нечитаемый текст на фронте.
Вы можете возразить, что текст дает нам свободу самовыражения: мы может записать код лесенкой или побаловаться ASCII art в комментариях. Это прекрасно, но имеет слабое отношение к программированию. Современные языки дают большую свободу для выражения сложных концепций в коде. Но если мы говорим о форматировании, для меня это выглядит странно: почему мы должны заботиться о пустых строках, табуляции или пробелах, ограничениях по длине строк и других несущественных деталях? Слава всем богам, мы не добавляем информацию о предпочитаемом шрифте и цвете в нашу кодовую базу, я точно не хочу читать код в Comic Sans.
Файловые системы
Еще один уровень абстракции между кодом и хранилищем. Зачем нам имена файлов и ограничения с ними связанные? Разве это удобно использовать двухбуквенные расширения для указания используемого языка? А ограничения на глубину вложенности каталогов или inotify watch limit? Или вот мы запускаем код в докере, а контейнер не видит изменения файлов на хосте, куда это годится?
А еще мы работаем на разных файловых системах, и они предоставляют нам разные функции. Чувствительность к регистру или нет? Жесткие и символические ссылки. Права доступа. Атрибуты. Все эти особенности не имеют отношения к нашему коду, по крайней мере, вы обычно не можете прочитать о них в спецификации вашего языка.
Я упомянул проблемы, с которыми сталкивался лично. Каждый раз мне приходилось тратить время на погружение в особенности файловых систем и поиск обходного пути. И далеко не всегда результат удовлетворял меня, чаще решение представляло собой костыль.
Парсинг
В спецификациях нашего языка задействовано много элементов форматирования: ключевые слова, скобки, разбиение строк, отступы. Возможно с ними проще читать код, но я уверен, что есть другой способ сделать код читабельным. Гораздо важнее эти элементы при парсинге кода компилятором и другими тулзами.
Обычно парсер разделяет ваш код на токены и строит Абстрактное Синтаксическое Дерево (AST). Затем он связывает идентификаторы, чтобы понять их назначение. Это очень быстрые операции, и ваш компьютер может разобрать множество файлов за секунды. Но не так много, как в вашем раздутом монорепозитории с миллионами строк кода. Поэтому иногда IDE тупит во время загрузки проекта.
Однако это еще не все, парсинг происходит много раз и после загрузки: при изменении кода, при переключении веток, при компиляции. В некоторых языках приходится парсить даже устанавливаемые зависимости (да, я про тебя, javascript). А еще у нас есть различные помощники для продуктивной работы: автодополнение, линтеры, статические анализаторы, форматтеры и AI-агенты. Все они читают наш код с диска и парсят его. Иногда используется один инструмент для всех этих задач, иногда разные. В таком случае их парсеры могут быть реализованы по-разному, что может привести к несогласованности: например, я иногда сталкивался с ситуацией, когда WebStorm сигнализирует об ошибках в импортах, но vite компилирует работающий JS без проблем.
Каким бы эффективным не был парсер, но он загружает ваш процессор и память. Сколько бы у вас не было ядер и планок памяти - они закончатся, когда вы откроете достаточно большой проект.
Диффы
Все становится намного хуже, когда вы работаете в команде. Мы используем специальное программное обеспечение для слияния изменений, но оно работает ужасно:
Когда вы перемещаете класс в другой файл — это новый класс.
Когда вы перемещаете функцию на несколько строк вверх или вниз — это новая функция.
Когда вы меняете имя любого идентификатора — у вас появляются изменения во всех файлах, которые его используют.
Когда два человека вносят изменения в один файл — возникают конфликты.
Основная проблема такого поведения заключается в том, что в наших текстовых файлах недостаточно информации о блоках кода — нет надежных идентификаторов, нет структуры. При сравнении двух файлов программное обеспечение может только распарсить оба файла и попробовать понять, какие блоки изменились, а какие нет. Но чаще сравнение происходит просто строчка за строчкой.
Итого
Итак, давайте представим, что мы разрабатываем систему для разработчиков, где они могут писать код, читать его и обмениваться изменениями. Должны ли мы использовать текстовые файлы в качестве основного источника истины? Очевидно, нет. Это неудобно, ненадежно и может привести к несогласованности.
Но что если мы распарсим код в AST, присвоим уникальные идентификаторы блокам кода, свяжем их и сохраним в графовую базу данных? Мы сможем сэкономить на размере данных если использовать enum для ключевых слов. Чтение из локальной БД будет сравнимо по скорости с чтением с диска. Форматирование AST должно быть быстрее парсинга. При слиянии веток надо будет сравнивать измененные блоки, а не файлы, что реже будет приводить к конфликтам. Статический анализ и автодополнение будут работать с блоками и связанными идентификаторами, что должно значительно уменьшить потребление памяти.
Система версионирования AST
Я работаю над PoC CVS, которая хранит AST кода TypeScript в распределенной графовой базе данных с коммитами и ветками.
Вот репозиторий: https://github.com/franzzua/ast
Текущий подход очень прост и прямолинеен. Код парсится с помощью oxc. В результате получается AST: функции, выражения, вызовы. Затем вычисляются области видимости для каждого блока: каждая функция создает область видимости, поэтому идентификаторы должны разрешаться от внутренней области к внешней.
Затем каждому узлу присваивается уникальный идентификатор и блоки связываются друг с другом, так что если есть вызов функции с именем sayHello, вместо ее имени ставится id вызываемой функции. После этого я сохраняю полученный граф в TerminusDB. Она требует схему данных, поэтому я попросил Gemini сгенерировать схему для AST oxc и подправил ее.
Сейчас эта штука может разбирать простой код в граф, сохранять его в базу данных, а затем загружать и форматировать как исходный текст. Дальше я собираюсь реализовать интерфейс для сравнения и слияния. Я представляю это как нечто вроде режима исправлений в Google Docs: некоторые части заменяются, другие перемещаются куда-то или удаляются. Надеюсь, получится сделать его интуитивным и понятным. После этого можно будет настроить автоимпорт проекта из git коммит за коммитом и посмотреть, где лучше и понятнее диффы.
Мечты о будущем
Никто не мешает расширить схему и добавить возможность привязать к блоку кода что-то вроде комментария, но с поддержкой Markdown - получится документация. А если добавить несколько полей и статусов, то можно реализовать менеджер задач. Конечно же нужно будет прикрутить CRDT для парного программирования. Небольшую доску для проектирования и чатик с войсами и стикерами. Why not?
Преимущества
Чистые диффы
Производительность и сокращение выбросов CO2
Согласованность между вашими инструментами