퍼지지 않은 최고의 아이디어
1984년, Donald Knuth는 급진적인 전환을 제안하는 논문을 발표했다. 프로그램은 컴파일러를 위해 작성되고 인간을 위해 주석을 다는 것이 아니라, 인간을 위한 문학으로 작성되어야 하며, 컴파일러는 그로부터 실행 가능한 부분을 추출해야 한다. 그는 이를 literate programming이라 불렀고, TeX를 이 방식으로 만들었다.
이 아이디어는 아름답다. 그리고 현대 소프트웨어 개발에서는 거의 완전히 사라져 있다.
TeX는 여전히 역사상 가장 신뢰할 수 있고 잘 이해된 대형 프로그램 중 하나이다. 이 성공은 부분적으로 Knuth가 이를 literate program으로 작성했기 때문이다. 따라서 문제는 literate programming이 작동할 수 있는지가 아니다. 분명히 가능하다. 문제는 왜 그것이 Knuth에게는 작동하고 다른 거의 모든 사람에게는 작동하지 않았는가이다.
Literate Programming이 실제로 의미하는 것
Knuth의 WEB 시스템, 이후 C용 CWEB는 단일 소스 파일을 두 가지 다른 출력으로 분할하는 방식으로 작동한다. 프로그램이 무엇을 하는지, 왜 그것을 하는지, 그리고 각 조각이 어떻게 맞춰지는지 설명하는 서사적 문서를 작성한다. 그 서사 안에 코드 조각들이 내장되어 있다.
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이다. 코드 리뷰 도구는 main.c의 줄을 이해한다. main.w의 서사적 청크는 이해하지 못한다. Syntax highlighting부터 정적 분석, CI 파이프라인에 이르는 도구 생태계는 컴파일 가능한 소스 파일을 기대한다. Literate programming은 이 모든 것에서 탈퇴하도록 요구한다.
기술 부조화
Knuth는 프로그래머도 작가라고 가정했다. 대부분은 아니다.
좋은 기술 문서는 드물다. 왜냐하면 어렵기 때문이다. 아직 당신이 이해하는 것을 이해하지 못하는 독자에 대한 공감이 필요하다. 무엇이 아니라 왜인지를 설명하는 규율이 필요하다. 코딩과는 별개의 기술인 편집이 필요하다.
팀에게 literate programs를 쓰라고 요구할 때, 당신은 그들에게 Knuth가 되라고 요구하는 것이다. 의도를 설명하고, 대안을 탐구하며, 독자를 추론을 통해 안내하는 광범위한 산문을 요구하는 것이다. 평균적인 코드 주석은 // TODO: fix this이다. 그것과 문학 사이의 간극은 도구의 문제가 아니다. 인간의 문제이다.
이것이 더 나은 도구로 literate programming을 부활시키려는 시도가 꾸준히 실패한 이유이다. 병목은 결코 weave와 tangle 도구 체인이 아니었다. 병목은 대부분의 프로그래머가 에세이를 쓰고 싶어하지 않고, 대부분의 코드베이스가 에세이 품질의 문서를 보상하지 않는다는 것이다.
실제로 그것을 대체한 것
업계는 Knuth의 목표를 포기하지 않았다. 업계는 다른 수단을 통해 더 약한 버전을 달성했다.
타입 시스템은 이전에 설명의 단락을 필요로 했던 의도를 이제 인코딩한다. 함수가 List<T> 대신 NonEmptyList<T>를 받아들일 때, type checker는 문서가 기술할 수 있었던 것 이상의 보증을 강제한다. Rust의 borrow checker가 참조 escape를 거부할 때, 그것은 산문으로는 여러 페이지를 필요로 할 제약을 전달한다.
Docstrings와 Javadoc, Rustdoc, TypeDoc과 같은 API 문서 도구는 중간 지대를 만들었다. 그들은 문서가 구조를 주도할 것을 요구하지 않고 문서를 코드 옆에 둔다. 소스나 생성된 문서를 읽을 수 있고, 둘 다 같은 파일에 있기 때문에 동기화된 상태를 유지한다.
테스트 스위트는 실행 가능한 문서가 되었다. 잘 쓰인 테스트는 “이 입력이 주어졌을 때 이 출력을 기대한다”고 산문이 결코 할 수 없는 것보다 더 정확하게 말한다. Property-based test는 불변 조건을 인코딩한다. Snapshot test는 의도를 포착한다. 테스트는 모든 커밋에서 실행되므로 산문처럼 벗어날 수 없다.
아마도 가장 중요한 것은 프로그래밍 언어 자체가 더 literate해졌다는 것이다. Python은 의사 코드처럼 읽힌다. Rust의 if let 구문은 pattern matching 의도를 직접 표현한다. 코드가 설명일 때, 코드에 대한 별도의 설명은 필요 없다.
실제로 작동한 현대판
Knuth의 비전이 거의 온전하게 살아남은 곳이 하나 있다: computational notebooks이다.
Jupyter notebooks, R Markdown, Observable은 산문과 코드가 단일 문서에서 공존할 수 있게 한다. 산문이 설명한다. 코드가 실행된다. 출력이 인라인으로 나타난다. 이것은 이름만 빼고 모든 면에서 literate programming이다.
Notebooks은 WEB이 실패한 곳에서 성공했다. 왜냐하면 맥락이 다르기 때문이다. 데이터셋을 탐색하는 데이터 과학자는 서사적 과정에 참여한다. 코드는 천천히 변한다. 독자는 결정을 이해하려는 인간 독자이다. 편집 주기는 리팩터링이 아니라 탐구이다. 소프트웨어 공학에서 literate programming을 죽인 제약은 데이터 과학에서는 특징이 된다.
교훈은 literate programming이 틀렸다는 것이 아니다. 이 아이디어는 좁은 생태적 틈새가 있다는 것이다. 프로그램이 연구 대상이 되는 완성된 산물일 때 번성한다. 프로그램이 변경되어야 하는 살아있는 시스템일 때 질식한다.
여기서 얻어야 할 것
아마도 다음 서비스에 CWEB를 채택하지는 않을 것이다. 괜찮다. 하지만 Knuth의 근본적인 통찰은 여전히 적용할 가치가 있다.
것들이 스스로를 설명하도록 이름을 지어라. process_data라는 함수는 WEB을 만지지 않았더라도 literate programming의 실패이다. remove_expired_sessions_older_than라는 함수는 자체 문서를 지닌다.
가졌으면 좋았을 설명을 써라. 코드의 한 섹션이 이해하는 데 산문 단락이 필요하다면, 그 단락을 써라. 주석, docstring, 또는 설계 문서에 넣어라. 매체는 설명하는 행위만큼 중요하지 않다.
도움이 될 때 서사적 구조를 컴파일러의 구조에서 분리하라. README-driven development, architecture decision records, RFC는 모두 도구 체인과 싸우지 않고 인간 중심의 이야기를 쓰는 방법이다.
Knuth는 우리가 책처럼 프로그램을 쓰길 원했다. 우리는 결국 프로그램에 대한 책을 썼다. 그것은 그가 기대했던 것보다 약한 승리지만, 여전히 승리이다.