Удаление мешающих абстракций

«…абстракция — это метод упорядочивания сложных компьютерных систем. Он работает, устанавливая уровень сложности, на котором человек взаимодействует с системой, подавляя более сложные детали ниже текущего уровня. Программист работает с идеализированным интерфейсом (обычно хорошо определенным) и может добавлять дополнительные уровни функциональности, которые в противном случае были бы слишком сложными для обработки».
— Википедия

Более десяти лет назад Чак Мур упомянул идею «ненужных абстракций», абстракций, которые затемняют природу лежащей в их основе концепции, а также немного рассказал о том, как он пишет код снизу вверх, определяя любые слова (инструкции) он считает полезным, меняя их по мере развития понимания.

Этот подход, кажется, глубоко отличается от того, что делает большинство из нас. Нас учат быть достаточно ленивыми и максимально полагаться на существующие библиотеки и инструменты. Ведь от нас часто требуют отправить как можно быстрее. И вообще я с этим не согласен. Создание сложных систем, включающих несколько доменов, само по себе сложно, поэтому чем больше кода мы сможем использовать повторно, тем лучше.

Тем не менее, я хочу выделить категорию инструментов, которые мы используем, и утверждаю, что важно уметь преодолевать слои абстракций, понимать и ценить задействованную «физику».

Это категория базы данных. Возможно, база данных является наиболее часто используемой и полезной абстракцией из всех. Он расширяет, сохраняет и разделяет память, доступную нашим приложениям. Базы данных, которые мы используем чаще всего, как правило, обладают широким набором функций. Запросы, индексирование, распространение, отказоустойчивость, резервное копирование и так далее. Эти вещи работают довольно хорошо (большую часть времени, в большинстве случаев). Так зачем нам вообще заглядывать в эти слои?

Как определила Википедия, абстракции «подавляют более сложные детали» и, следовательно, удаляют понимание того, как все работает, дальше от нас, пользователей абстракции. Хотя, конечно, это не тайна или особенно секретное знание того, как работает какая-либо конкретная база данных (особенно если вы используете открытый исходный код), в условиях физических и психологических ограничений сроков так заманчиво сосредоточиться на абстракции базы данных. себя и не думать о сложности. В конце концов, все это сложно.

В результате мы получаем плохо смоделированные домены, неэффективные запросы и начинаем искать решения, которые могут быть неправильными.

Я предполагаю, что, стряхнув несколько слоев (довольно непрактично стряхивать их все), мы можем немного приблизиться к «металлу» и по-настоящему оценить то, что связано с хранением, извлечением и поиском данных.

Уровень абстракции, на котором я решил здесь остановиться, — это база данных ключ/значение дерева B+. Вы можете искать начало, конец, конкретный ключевой префикс и переходить к предыдущему или следующему ключу. Я бы сказал, довольно компактная поверхность абстракции. В качестве дополнительного поворота, будучи сторонником источников событий, функционального программирования и неизменности, я сам установил одно правило: никаких перезаписей. Я могу связать значение с ключом, но не могу изменить его после этого.

Если вы знакомы с моей недавней работой с открытым исходным кодом, да, я описываю PumpkinDB.

Я хочу показать вам небольшой пример. Пожалуйста, смиритесь с его легкой искусственностью и почти намеренной глупостью (надеюсь, что однажды у меня будет лучше!). На этот раз я играл с дизайном простого списка дел. Вы можете добавлять элементы в список, изменять их описание, отмечать их или отменять. Для простоты, ни удаления, ни переупорядочения.

Как бы я реализовал это, учитывая описанную выше архитектуру и ограничения?

Оказывается, это не очень сложно.

Что тут делать в первую очередь? Ну, очевидно, если мы не будем добавлять элементы в список, это будет не очень полезный список. Поэтому нам нужно записать несколько пар ключ/значение, которые указывают именно на это. Предположим, что каждому новому элементу мы присваиваем случайный UUID. Как мы можем записать это таким образом, чтобы мы могли легко найти все добавленные элементы?

Одним из способов было бы объединить некоторый общий префикс (скажем, AddTodoItem) с UUID, оставив связанное значение пустым. Но если вы посмотрите на диаграмму ниже, вы обнаружите, что там что-то другое. Дело в том, что хотя приведенная выше структура действительно позволяет нам находить все добавленные элементы задач, они будут появляться в случайном порядке (в конце концов, эти UUID случайны).

Одна вещь, которую мы можем сделать, это добавить метку времени к значению и отсортировать элементы после их извлечения. Но вряд ли это звучит как эффективный подход. Вместо этого мы можем переместить UUID в значение и добавить к ключу какую-то уникальную и монотонно увеличивающуюся временную метку. Гибридные логические часы — хороший кандидат для этого. Теперь можно быстро просмотреть все пары ключ/значение с префиксом AddedTodoItem и выяснить, что находится в списке, в каком порядке и когда именно элементы были добавлены.

В PumpkinScript это будет что-то вроде "AddedTodoItem" [DUP CURSOR/KEY SWAP CURSOR/VAL TRUE] CURSOR/DOWHILE-PREFIXED. Для каждой пары ключ/значение с префиксом AddedTodoItem извлеките ключ и значение.

Точно так же мы можем разработать макет для изменения описания и статуса элемента (ChangedTodoItem и ChangedItemStatus). Основное отличие в составе ключей здесь заключается в том, что здесь мы используем три элемента (префикс, ссылку на элемент UUID и отметку времени) вместо двух.

Теперь, несмотря на то, что мы можем сортировать отдельные типы событий, если мы хотим увидеть агрегированный список событий для каждого элемента, это снова будет довольно неэффективно, так как нам нужно будет получать все события с определенным UUID, на который ссылается ключ, а затем объединить-сортировать все их по метке времени. Вместо этого мы можем ввести еще одно ключевое пространство. Ключи, начинающиеся с UUID, должны сопровождаться отметкой времени, а значение должно быть ключом определенного события. В PumpkinScript получение из этого списка по-прежнему довольно тривиально: uuid [DUP CURSOR/KEY SWAP CURSOR/VAL DUP RETR TRUE] CURSOR/DOWHILE-PREFIXED.

Как вы можете видеть, используя этот подход, мы действительно можем сказать, что происходит — какую часть ключевого пространства мы итерируем, сколько данных мы дублируем и так далее. Конечно, на этом уровне мы не знакомимся с механикой дисков, подкачки памяти или доступа к контроллеру [NVMe]. Для большинства приложений такого уровня абстракции действительно было бы слишком много. Тем не менее, возможно, уровень B+-дерева достаточно прост для работы, и в то же время он говорит нам гораздо больше о том, как все работает, и о том, насколько это сложно.

Если вам интересно изучить эти и подобные идеи, я буду рад, если вы ознакомитесь с проектом PumpkinDB. Мы тоже тусуемся на Гиттере и не против ответить на вопросы!