Лучшая идея, которая так и не прижилась
В 1984 году Donald Knuth опубликовал статью, в которой предложил радикальное изменение. Программы не должны писаться для компиляторов и комментироваться для людей. Они должны писаться как литература для людей, из которой компиляторы извлекают исполняемые части. Он назвал это literate programming и создал TeX именно таким образом.
Идея прекрасна. И она также почти полностью отсутствует в современной разработке программного обеспечения.
TeX по-прежнему остаётся одной из самых надёжных и хорошо понятых больших программ, когда-либо написанных. Этот успех отчасти объясняется тем, что Knuth написал её как literate program. Так что вопрос не в том, может ли literate programming работать. Она явно может. Вопрос в том, почему она сработала у Knuth и почти ни у кого больше.
Что на самом деле означает literate programming
Система WEB Knuth, позже CWEB для C, работает путём разделения одного исходного файла на два разных выхода. Вы пишете повествовательный документ, объясняющий, что делает программа, почему она это делает и как части сочетаются друг с другом. Встроенные в это повествование — фрагменты кода.
Инструмент tangle извлекает фрагменты кода, упорядочивает их для компилятора и создаёт файл, который машина может запустить. Инструмент weave извлекает прозу, форматирует фрагменты кода в виде красиво оформленных блоков и создаёт документ, который может прочитать человек.
Вот как выглядит небольшая программа на CWEB на практике:
@* Introduction. This program computes the sum of a list of integers.
The algorithm is straightforward: iterate through the array and accumulate.
@c
#include <stdio.h>
@<Function prototypes@>;
@<Main program@>;
@*2 The core logic.
We define |sum_array| to take a pointer and a length, returning the total.
@<Function definitions@>=
int sum_array(const int *arr, size_t n) {
int total = 0;
for (size_t i = 0; i < n; i++) {
total += arr[i];
}
return total;
}
@*2 Entry point.
A small driver to exercise the function.
@<Main program@>=
int main(void) {
int data[] = {1, 2, 3, 4, 5};
size_t len = sizeof(data) / sizeof(data[0]);
printf("%d\n", sum_array(data, len));
return 0;
}
Обратите внимание на происходящее. Код организован не по порядку компиляции. Он организован по повествовательной логике. Объяснение идёт первым. Реализация следует, когда читатель к ней готов. Синтаксис @<Main program@> — это именованный фрагмент, который может быть определён в любом месте документа и собран инструментом tangle в правильном порядке для компилятора.
Это центральная идея: человек читает объяснение сверху вниз. Компилятор получает граф зависимостей снизу вверх. Каждый получает предпочитаемый формат.
Почему это сработало у Knuth
Knuth — не типичный программист. Он математик и писатель, который случайно пишет программы. Он проектирует алгоритмы на бумаге месяцами, прежде чем прикоснуться к клавиатуре. Он пишет книги, которые читают десятилетия спустя. Когда Knuth говорит, что код должен быть литературой, он имеет в виду настоящую литературу, и у него есть навыки, чтобы это реализовать.
TeX также был идеальным кандидатом. Это пакетная программа со стабильной спецификацией. Алгоритмическое ядро, особенно процедуры разбиения на строки и страницы, получает огромную выгоду от подробных математических объяснений. Код меняется медленно. Документация переживает реализацию.
Большинство программных продуктов не таковы. Большинство программ меняются ежедневно. Спецификация открывается, а не проектируется. Аудитория — не читатель, пытающийся понять алгоритм. Это разработчик, пытающийся исправить ошибку до ежедневного совещания.
Проблема редактирования
Literate programming предполагает процесс написания. Большая часть программирования — это процесс редактирования.
Когда вы пишете эссе, вы составляете черновик, пересматриваете и полируете. Структура спланирована. Когда вы пишете программное обеспечение, вы исследуете, тестируете, проводите рефакторинг и выпускаете. Структура возникает сама. Документация в literate program — не покрытие, которое вы наносите впоследствии. Это первичная структура. Каждый раз, когда вы переименовываете переменную, выделяете функцию или меняете порядок логики, вы переписываете литературу.
Это создаёт порочный круг. Если проза тесно связана с кодом, рефакторинг становится дорогим. Если проза слабо связана, она отдаляется от кода и становится неверной. В любом случае документация устаревает.
Современная система контроля версий усугубляет это. Literate program — это единый повествовательный документ. Pull request — это diff по дереву исходных файлов. Инструменты code review понимают строки в main.c. Они не понимают повествовательные фрагменты в main.w. Экосистема инструментов, от подсветки синтаксиса до статического анализа и CI-пайплайнов, ожидает компилируемые исходные файлы. Literate programming требует отказа от всего этого.
Несоответствие навыков
Knuth предполагал, что программисты также являются писателями. Большинство — нет.
Хорошее техническое письмо редко встречается, потому что оно сложное. Оно требует сопереживания читателю, который ещё не понимает то, что понимаете вы. Оно требует дисциплины объяснять почему, а не только что. Оно требует редактирования, которое является отдельным навыком от программирования.
Когда вы просите команду писать literate programs, вы просите их быть Knuth. Вы просите развёрнутую прозу, объясняющую намерения, исследующую альтернативы и ведущую читателя через рассуждения. Средний комментарий в коде — // TODO: fix this. Разрыв между этим и литературой — не проблема инструментов. Это человеческая проблема.
Вот почему попытки возродить literate programming с помощью лучших инструментов систематически терпели неудачу. Узким местом никогда не была toolchain weave и tangle. Узким местом является то, что большинство программистов не хотят писать эссе, и большинство кодовых баз не поощряют документацию эссе-качества.
Что на самом деле её заменило
Отрасль не отказалась от цели Knuth. Она достигла более слабой версии этой цели другими средствами.
Системы типов теперь кодируют намерение, которое раньше требовало абзацев объяснений. Когда функция принимает NonEmptyList<T> вместо List<T>, type checker обеспечивает гарантию, которую документация могла только описать. Когда borrow checker Rust отклоняет выход ссылки за пределы области видимости, он сообщает об ограничении, на объяснение которого в прозе ушли бы страницы.
Docstrings и инструменты документирования API, такие как Javadoc, Rustdoc и TypeDoc, создали компромисс. Они сохраняют документацию рядом с кодом, не требуя, чтобы она определяла структуру. Вы можете читать исходный код или сгенерированную документацию, и оба остаются синхронизированными, потому что находятся в одном файле.
Наборы тестов стали исполняемой документацией. Хорошо написанный тест говорит «при данном входе ожидайте данный выход» точнее, чем это могла бы когда-либо сделать проза. Property-based tests кодируют инварианты. Snapshot tests фиксируют намерения. Тесты запускаются при каждом коммите, поэтому они не могут отклоняться, как проза.
Возможно, что самое важное: сами языки программирования стали более литературными. Python читается как псевдокод. Синтаксис if let в Rust напрямую выражает намерение pattern matching. Когда код является объяснением, вам не нужно отдельное объяснение кода.
Современная версия, которая сработала
Есть одно место, где видение Knuth сохранилось почти неизменным: computational notebooks.
Jupyter notebooks, R Markdown и Observable позволяют прозе и коду сосуществовать в одном документе. Проза объясняет. Код выполняется. Вывод появляется inline. Это literate programming во всём, кроме названия.
Ноутбуки добились успеха там, где WEB потерпел неудачу, потому что контекст другой. Data scientist, исследующий набор данных, занят повествовательным процессом. Код меняется медленно. Аудитория — человек, пытающийся понять решение. Цикл редактирования — исследование, а не рефакторинг. Ограничения, которые погубили literate programming в программной инженерии, в data science являются преимуществами.
Урок не в том, что literate programming была неправой. Урок в том, что у этой идеи есть узкая экологическая ниша. Она процветает, когда программа — это законченный артефакт, предназначенный для изучения. Она задыхается, когда программа — это живая система, предназначенная для изменений.
Что из этого извлечь
Вы, вероятно, не собираетесь внедрять CWEB для своего следующего сервиса. Это нормально. Но базовое понимание Knuth всё ещё стоит применять.
Называйте вещи так, чтобы они объясняли сами себя. Функция с именем process_data — это провал literate programming, даже если вы никогда не касались WEB. Функция с именем remove_expired_sessions_older_than несёт в себе собственную документацию.
Пишите объяснение, которое вам хотелось бы иметь. Если раздел кода требует для понимания абзаца прозы, напишите этот абзац. Поместите его в комментарий, docstring или документ проектирования. Среда менее важна, чем сам акт объяснения.
Отделяйте повествовательную структуру от структуры компилятора, когда это помогает. README-driven development, architecture decision records и RFC — это всё способы написать ориентированную на человека историю, не сражаясь со своей toolchain.
Knuth хотел, чтобы мы писали программы как книги. В итоге мы стали писать книги о программах. Это более слабая победа, чем он надеялся, но всё равно победа.