Lobsters

Design your programming languages right (2024)

프로그래밍 언어를 제대로 설계하기

새 프로그래밍 언어가 초기에 결정해야 할 설계 원칙을 정리합니다. 들여쓴 여러 줄 문자열, 파일 경로와 확장자, 후행 쉼표, 키워드와 숫자 리터럴, 개발·배포 환경, 버전 관리, 빌드 시스템과 파일 감시까지 생태계와 호환성을 좌우하는 세부 사항을 짚습니다.

AI 요약

새 프로그래밍 언어를 만들 때 문법 자체만큼 일찍 결정해야 하는 설계 항목이 많습니다. 뒤늦게 바꾸면 하위 호환성 문제와 도구 생태계의 비용이 커지므로, 사용자가 실제로 코드를 작성하고 검색하고 배포하는 흐름까지 고려해야 합니다.

들여쓰기와 경로를 문법에 포함하기

여러 줄 문자열은 코드의 현재 들여쓰기를 망가뜨리기 쉽습니다. 문자열 내용이 왼쪽 여백을 차지하면서 주변 코드의 구조가 흐려지기 때문입니다. 원문은 이 문제를 이미 해결한 사례로 Dhall을 들고, Nix와 Lix도 비슷한 규칙을 사용한다고 설명합니다. JavaScript template literal에는 Ve.dedent를 적용했고, PureScript와 Python에서는 소스 텍스트와 이스케이프·보간된 텍스트를 구분하기 어려워 일부 경우에만 작동하는 도구가 되기 쉽다고 말합니다. 언어 차원에서 들여쓴 여러 줄 문자열을 지원하면 사용자가 늘어난 뒤 문법을 바꾸는 부담을 피할 수 있습니다.

파일 경로도 기준을 일찍 정해야 합니다. 설정 파일의 경로는 해당 설정 파일이 있는 위치나 명확하게 정의한 프로젝트 루트를 기준으로 삼아야 하며, 실행 프로그램의 작업 디렉터리와 섞으면 안 됩니다. 원문은 Docker Compose와 Rust Cargo의 설정 파일 경로 처리를 잘못된 사례로 언급합니다. 경로 기준을 뒤늦게 고치려면 버전 관리와 마이그레이션 계획이 필요하지만, 초기 설계가 부실한 언어는 이런 변경을 뒷받침할 버전 정책도 부족한 경우가 많다고 지적합니다.

확장자와 작은 문법 결정

새 언어는 기존 도구 생태계에 들어가는 손님이므로 파일 확장자를 한두 개로 정하고 일관되게 사용해야 합니다. IDE, 온라인 코드 뷰어, Git forge의 syntax highlighting은 주로 확장자에 의존합니다. 확장자가 없거나 파일 이름 종류가 지나치게 많으면 사용자가 편집기 설정을 따로 고쳐야 하고, grep 검색에도 가능한 파일 이름을 모두 넣어야 합니다.

Bazel은 Starlark 파일을 WORKSPACE, BUILD, .bzl, .bazel 같은 이름으로 구분합니다. Starlark가 Python 문법 강조를 활용하는 점은 편리하지만, 확장자와 확장자 없는 파일 이름이 섞이면 IDE가 모든 파일을 제대로 강조하기 어렵습니다. 파일 이름을 사용자가 설정하게 만들면 이 문제는 더 커집니다.

후행 쉼표(trailing comma)는 허용하는 편이 낫다고 말합니다. 필요하다면 선행 쉼표나 실제 목록 표기 방식도 검토할 수 있습니다. 숫자 리터럴의 접두사·접미사, 문자열과 정규식의 escape 규칙, 키워드도 초기에 신중히 정해야 합니다. 예를 들어 escape를 \\u{XXXX}처럼 구분된 형태로 만들면 확장 여지가 생깁니다.

키워드 앞에 별도 기호(sig­il)를 붙이자는 주장은 필자의 개인적인 의견입니다. bare keyword를 사용하면 새 키워드를 추가할 때 기존 코드에서 같은 이름을 식별자로 사용한 부분이 깨질 수 있다는 이유입니다. 다만 댓글에서는 이런 방식이 지나치게 과하다는 반론이 나왔습니다. 키워드에 기호를 붙이는 방식보다, 하위 호환성 변경을 명확히 정의하고 기계적으로 고칠 수 있는 규칙을 마련하는 편이 낫다는 의견입니다.

개발 모드와 배포 모드

코드를 만드는 과정과 정리된 릴리스 버전은 서로 다른 작업 흐름으로 다뤄야 합니다. 개발 중인 웹 서버는 다른 포트나 LAN 호스트에서 HTTP, self-signed HTTPS, 경우에 따라 file:// URL을 사용합니다. 배포 환경에서는 reverse proxy 뒤의 특정 경로에 배치될 수 있습니다. 언어와 도구가 두 환경의 차이를 전제로 설계되면 개발 중인 코드와 배포된 코드 사이의 설정 차이를 더 분명하게 다룰 수 있습니다.

버전, 마이그레이션, 릴리스 날짜

버전 간 코드는 별도의 문제를 만듭니다. 예를 들어 새 버전으로 데이터를 옮기는 migration script는 이전 버전이 아직 존재할 때 작성할 수 없으므로 보통 이후 버전에 들어갑니다. 그렇다면 테스트할 때 이전 버전의 소스와 새 버전의 소스를 source control에서 동시에 checkout해야 합니다. 원문은 이 작업이 개발 모드와 릴리스 모드의 차이를 잘 보여준다고 설명합니다.

라이브러리를 배포하는 언어라면 버전을 표현하고 비교하는 방법도 제공해야 합니다. 타입이 있는 라이브러리의 API를 검사해 patch, minor, major 중 어떤 변경인지 판단하는 도구가 좋은 사례로 제시됩니다. 다만 버전 번호는 수학적으로 정확한 분류가 아니라 판단을 전달하는 불완전한 의사소통 수단입니다. 릴리스 버전에는 날짜도 함께 노출해야 합니다. 버전 번호에 날짜를 넣지 않더라도 버전 옆이나 tooltip, 공식 웹페이지에서 쉽게 확인할 수 있어야 합니다. 여러 언어, runtime, package, dependency의 버전을 비교하려면 릴리스 시점을 알아야 하기 때문입니다.

빌드 시스템과 검색 도구

파일 감시(file watching)는 나중에 덧붙이기 어렵습니다. 파일 간 의존성, 각 파일의 사용처, 설정 변경, 새 파일 추가를 모두 고려하려면 별도의 Python 프로그램으로 파일을 파싱하고 의존성 트리를 다시 구성해야 할 정도로 일이 커집니다. 처음부터 빌드 시스템이 파일과 glob 목록을 제공하면 사용자는 그 목록을 검색에 재사용하고, dependency를 검색 대상에 포함할지도 선택할 수 있습니다.

파일 목록은 파일 감시의 출발점이기도 하지만, 새 파일과 설정 변경까지 처리하려면 빌드 시스템과 감시 기능의 통합이 더 필요합니다. 나아가 code search가 식별자와 문자열, 타입과 일반 표현식을 구분하면 단순한 텍스트 검색보다 정밀한 탐색이 가능합니다. 언어 설계가 컴파일러 문법에서 끝나지 않고 IDE, 검색, 빌드 도구의 입력 형태까지 이어져야 한다는 주장입니다.

Lobsters 반응

  • @icefox — 대체로 좋은 목록입니다. 개인적으로 sum type을 추가하겠습니다. 그리고 if, for, while 뒤의 {}를 생략하도록 허용하는 C의 방식을 쓰지 않았으면 합니다. 귀여운 문법 해킹이지만 지금까지 한 번도 그 비용을 만회하지 못했습니다.
    • @icefox — 키워드에 기호를 붙이자는 주장은 확실히 대담합니다. 저는 $fn sum(values: &[i32]) -> i32 { $let $mut accm = 0; $for item $in values { accm += item; } $return item; }처럼 쓰고 싶지는 않습니다. 오히려 fn sum($values: &[i32]) -> i32 { let mut $accm = 0; for $item in $values { $accm += $item; } return $item; }처럼 쓰는 편이 낫습니다. 하지만 제 생각에는 둘 다 필요하지 않습니다. 하위 호환성을 깨는 변경에 관한 명확한 체계와 기대치를 정하는 편이 좋습니다. 이런 문제는 기계가 찾아서 보통 고칠 수 있습니다.

원문: Veritates / 번역·요약: Trawling