C3 배우기
(alloc.dev)- C3는 C와 함께 쓰는 시스템 언어를 목표로 하며, 모듈 시스템, 제네릭, 컴파일 타임 실행, 오류 처리,
defer, 슬라이스,foreach같은 기능으로 C 개발자의 전환을 쉽게 만듦 - 문법은 C에 가깝지만
fn,foreach,defer, implicitbreak,nextcase, 타입 메서드, 계약(contracts)처럼 표현력과 안전성을 보강하는 장치가 많음 - Gentoo에서 소스 빌드를 시도할 때 LLD 라이브러리 탐지 문제가 있었고, 결국 사전 빌드된
c3c 0.7.1로c3c init,c3c run흐름을 확인함 - 계산기 구현 과정에서
tmem임시 할당자,@pool, 표준 라이브러리List, optional 오류 처리,!재전파,nextcase가 유용했지만 언어 서버 지원 부족과 양끝 포함 슬라이싱은 불편했음 - C3는 C보다 안전하고 C++, Rust보다 단순하며 빠르게 개발할 수 있는 대안으로 보이지만, 매일 쓸 언어로는 Zig를 더 선호하고 C 대체 후보로 계속 써볼 만함
C3가 노리는 위치
- C3는 C를 기반으로 삼으면서 C와 함께 작동하는 언어를 목표로 하며, 표준 C에서 표현하기 어렵거나 불가능한 기능을 제공함
- 주요 기능은 모듈 시스템, 연산자 오버로딩, 제네릭, 컴파일 타임 실행, 의미론적 매크로, 통합 빌드 시스템, 오류 처리,
defer, 값 메서드, enum 연관 데이터, distinct type과 subtype, 점진적 계약, 내장 슬라이스,foreach, 동적 호출과 타입 등임 - 학습 과정은 실시간 기록에 가까워 장점과 불편함이 바로 드러나지만, 언어 이해가 다른 자료보다 부정확할 수 있다는 전제가 있음
C와 닮았지만 더 많은 기본기
- Hello World 예제는
import std::io;,fn void main(),io::printn("Hello, World!");형태라 C와 비슷한 첫인상을 줌 - C3의 import는 하위 모듈을 재귀적으로 가져올 수 있고, 이름 충돌이 생기면
abc::Context처럼 더 완전한 이름으로 해결함 - 표준 출력 함수는 전달된 대부분의 타입을 출력할 수 있어 print 디버깅이 C보다 편해 보임
printf는%s,%d,%f같은 C 스타일 포맷을 유지함%s는 enum 값REALLY_WARM도 문자열처럼 출력할 수 있음
제어 흐름: foreach, switch, nextcase
foreach는 슬라이스 값을 순회하며, 변수명 앞에&를 붙이면 참조 기반 반복이 가능함break와continue는 예상한 방식으로 동작함- 일반
for보다 의도를 잘 표현해 로직 오류를 줄일 수 있지만, C스럽지 않다는 인상도 남김
while조건 안에서 변수를 선언할 수 있어 C의 일반적인while보다 확장된 형태를 가짐switch는 C와 닮았지만 implicit break를 제공함- 빈
case는 허용되지 않아 명시적break가 필요함 - enum 전체 값을 이미 처리한 뒤
default를 두면 컴파일러가 경고함 - 각
case는 자체 스코프를 시작함
- 빈
nextcase는 다음 case로 fallthrough하는 키워드이며, 런타임 case를 지정해 jump table처럼 사용할 수 있음- Duff's device 예제를 C3에서 깔끔하게 작성할 수 있음
@jump속성은 최적화 컴파일러가 switch를 jump table로 만들도록 강제함
defer와 리소스 정리
defer는 스코프 종료 시 역순으로 실행되며, 주로 리소스 정리에 쓰임- 리소스를 획득한 직후
defer를 배치하면 모든 종료 경로에서 정리를 기억해야 하는 부담이 줄어듦 - C에서 흔히 쓰는 함수 끝의
gotocleanup 블록보다 훨씬 낫다고 평가함 errdefer와okdefer에 해당하는 동작은 각각defer catch,defer try로 표현할 수 있음
구조체, 메서드, 타입 정보
struct는 필드, 함수 포인터, enum, named sub-struct, anonymous sub-struct, anonymous union, named sub-union을 포함할 수 있음- 구조체 내부의 anonymous
union은 enum과 함께 tagged union을 구현하는 깔끔한 방식으로 보임 - C에서도 비슷한 구성이 가능하지만, C3 예제는 구조체 안에서 데이터를 저장하고 접근하는 방식이 더 명확함
- C3는 구조체, union, enum뿐 아니라 primitive를 포함한 모든 타입에 메서드 형태의 dot syntax 함수를 붙일 수 있음
- 모든 타입에는
.alignof,.kindof,.nameof,.sizeof,.typeid,.methodsof,.is_eq,.is_ordered같은 컴파일 타임 프로퍼티가 있음- 이 프로퍼티들은 메타프로그래밍에 유용함
- Zig에서 비슷한 정보를 얻으려면
@typeInfo를 사용해야 함
오류 처리와 optional
- C3의 optional 타입은 타입 뒤에
?를 붙여 표현함int? a = 1은 값이 있는 optional임- optional이 비어 있으면 원인을 설명하는 Excuse를 가짐
faultdef로OOPS,USER_ERROR같은 fault를 정의할 수 있음
catch로 optional이 비어 있는지 검사하고,!연산자로 오류를 재전파함- optional을 검사한 뒤 해당 스코프를 빠져나가면 남은 스코프에서는 자동으로 unwrap됨
- optional이 식에 포함되면 결과도 optional이 됨
- C3는 Zig의
error{SomeError}!i32와?i32, Rust의Result<i32, SomeError>와Option<i32>역할을 하나의 시스템으로 합친 것처럼 보임 - 이 설계는 “반드시 값이 기대되지만 실패할 수 있는 함수”와 “값이 없을 수 있는 함수”의 구분을 흐리게 함
- 대신 반환값을 처리할 때 두 타입 체계를 구분할 필요가 없다는 장점도 있음
계약과 매크로
- C3는
<*와*>안에 precondition/postcondition 계약을 둘 수 있음@require foo != null@ensure return > foo.x
- 계약은 파싱되는 주석 형태이며, 단순 런타임 assertion을 넘어 호출 체인으로 조건을 전파하고 컴파일 타임 folding 중 분석됨
- 현재 C3는 컴파일 타임 folding을 넘어서는 정적 분석은 수행하지 않음
- C3 매크로는 C 매크로처럼 컴파일 타임에 평가될 수 있지만,
macro키워드를 사용하고 C의 줄 끝\문제를 피함 - 매크로 파라미터에는 특수 접두사가 있음
$는 컴파일 타임에 알려진 값임#는 아직 평가되지 않은 식이며, 변수 전달 시 한 번 평가된 뒤 참조 위치에 직접 사용됨$나#파라미터를 쓰는 사용자 정의 매크로는@접두사가 필요함
- 컴파일 타임 변수만 가진 매크로는 완전히 컴파일 타임에 평가된다는 보장이 있음
- 타입 리플렉션과 컴파일 타임 실행은 매크로를 통해 처리됨
- C3 매크로는 C의 AST 조작 수준의 힘은 덜하지만, Zig의 comptime과 C 매크로의 혼합처럼 느껴짐
리터럴과 primitive 타입
- C3는 base64와 hex 데이터 리터럴을 지원함
b64"SGVsbG8gV29ybGQh"x"4865 6c6c 6f20 776f 726c 6421"
- 일반적인 사용 사례는
$embed매크로로도 충족될 수 있어, 자주 쓰이지 않는 기능에 문법 설탕을 추가한 느낌이 있음 - 문자열 전반은 C와 Zig처럼 “문자열은 바이트”라는 접근을 선호하는 입장에서는 다소 조심스럽게 느껴짐
- primitive 타입은 정수, bool, float 등을 포함하며 C와 대체로 비슷함
char는 unsigned로 정의되어 C처럼 컴파일러에 signedness가 맡겨지지 않음int128,uint128도 제공됨iptr,uptr,isz,usz는 짧지만 낯선 사람에게는 읽기 비용이 있을 수 있음- 다른 primitive 타입의 비트 크기가 명확하게 정의되어 C보다 개선된 점으로 평가함
설치와 프로젝트 생성 경험
- C3 웹사이트는 컴파일러 바이너리 설치 방법과 소스 빌드 방법을 제공함
- Gentoo 환경에서는 C3 패키지를 찾지 못해 소스 빌드를 시도함
git clone https://github.com/c3lang/c3c.gitcmake ..make
- 빌드 과정에서 CMake가
LLD_COFF,LLD_COMMON,LLD_ELF,LLD_MACHO,LLD_MINGW,LLD_WASM을 찾지 못함 - 설치된
lld 20.1.5에는 C3가 의존하는 LLD 라이브러리가 포함되지 않아 문제가 발생했고, 결국 사전 빌드된 C3 컴파일러를 사용함 - 사용한 컴파일러는
C3 Compiler Version: 0.7.1, LLVM backend, LLVM17.0.6, targetx86_64-pc-linux-gnu로 표시됨 c3c init hello_world는 프로젝트 디렉터리와LICENSE,README.md,project.json,src/main.c3등을 생성함LICENSE와README.md는 비어 있음- 빈
LICENSE파일은 일반 permissive license가 들어 있을 것으로 기대했다면 놓치기 쉬운 점임
c3c run은 프로젝트를 빌드하고 실행해Hello, World!를 출력함c3c init,c3c build,c3c compile흐름은 Zig의zig init,zig build,zig build-exe를 떠올리게 함
계산기 구현에서 드러난 실사용 감각
- 계산기 예제는
+,-,*,/, 단항-,^, 괄호를 처리하는 기본 계산기를 목표로 함 - 사용자 입력은
io::treadline()으로 받고, 반환값은String?optional로 처리함 - C3에는 기본 할당자로
tmem과mem이 있으며,tmem은 arena allocator처럼 동작하는 임시 할당자임@pool()범위 안에서tmem으로 할당한 항목은 스코프 종료 시 해제됨- 표준 라이브러리의 여러 함수는
t접두사로 임시 할당자 사용을 나타냄
- 이 방식은 Zig처럼 allocator를 명시적으로 넘기는 관행과 C처럼 표준 allocator를 쓰는 관행의 중간에 가까움
main이int를 반환하면 C3는 끝에서 값을 반환하도록 요구함- 반환이 빠지면 “Missing return statement at the end of the function” 오류가 발생함
main을void로 표시하는 것도 가능함
io::printf와io::printfn은 반환값을 반드시 처리하지 않아도 되며, 이는@maydiscardannotation 때문임- 반대 annotation으로
@nodiscard도 있음
- 반대 annotation으로
토크나이저 구현에서 본 장단점
- 토크나이저는 입력 문자열을
Token[]로 변환하며,TokenTagenum으로ADD,SUBTRACT,MULTIPLY,DIVIDE,POWER, 괄호,NUMBER,DELIMITER를 정의함 - 숫자는 문자열에서 숫자와
.을 스캔한 뒤to_float()로 변환함 - 표준 라이브러리의
std::collections::list와List {Token}은 토큰을 누적하는 데 편리함 - 익숙한 C 스타일
for루프와foreach루프가 함께 잘 작동함for는 스캔 로직의 중심에 쓰임foreach는 토큰 출력에 쓰임
- 언어 서버는 표준 라이브러리 코드 자동완성과 정의 이동이 작동하지 않아 작성 중 어려움이 있었고, 언어가 성숙하면 개선될 부분으로 봄
- 슬라이싱 문법은
some_string[start..end]형태이며 시작과 끝 인덱스가 모두 inclusive임- 대부분의 다른 언어와 달라
end - 1조정이 필요함 - 길이 0 slice를 이 방식으로 만들 수 없어 혼란스러움
- 이후
some_string[start:length]형태의 slice-by-length 문법이 있다는 점을 확인함
- 대부분의 다른 언어와 달라
tokenize함수 안에서 전달받은 allocator와 동시에tmem을 사용할 수 있어, 중간 처리와 반환값 할당을 분리하기 좋음
파서 구현과 오류 전파
- 파서는 재귀 하강 파서(recursive descent parser)로 작성됨
Parser구조체는Token[] source와usz index를 가지고,Parser.parse,Parser.expression,Parser.term,Parser.factor,Parser.negation,Parser.number메서드로 문법을 나눔- 문법은 다음 형태를 따름
<expression> ::= <term> (("+" | "-") <term>)*<term> ::= <factor> (("*" | "/") <factor>)*<factor> ::= <negation> ('^' <factor>)*<negation> ::= "-" <negation> | <number><number> ::= '(' <expression> ')' | <floating point number>
- 오류는
faultdef UNEXPECTED_TOKEN과 optional 반환으로 처리함 p.term()!,p.factor()!같은 재전파 덕분에 오류가 없는 코드처럼 작성하면서 잘못된 입력은 바깥으로 전파됨nextcase는expression과term에서 반복적인+,-,*,/처리를 깔끔하게 만드는 데 잘 맞았음- 예제 입력
2+3 * (-7/-4) ^ 3.14는Result: 19.388445를 출력함
최종 평가
- C3는 C의 맛을 유지하지만 같은 뒷맛은 아니며, 작업 자체는 재미있음
- C++와 Rust보다 단순하고, Rust, C++, Zig보다 개발 속도가 빠르며, C보다 안전하고 표현력이 높다고 평가함
- 매일 사용할 언어로 선택할 가능성은 크지 않고, 개인적으로는 Zig 작업을 더 선호함
- C로 작성될 프로젝트의 대안으로 C3를 사용할 수 있으며, 짧은 사용만으로도 잠재력을 보았고 계속 사용할 의향이 있음
- C3 표준 라이브러리 소스는 읽기 쉬웠고, 매크로 시스템은 다른 매크로 시스템보다 선호함
- 여전히 마음에 들지 않는 부분으로 양끝 포함 슬라이싱 문법과 오류 시스템이 남음
- C3를 시도하려는 사람에게는 더 좋아질 때까지 기다리기보다 직접 써보는 쪽을 권함
댓글과 토론
Hacker News 의견들
- C3 관련해서 흥미로울 만한 다른 링크들임
인터뷰: https://www.youtube.com/watch?v=UC8VDRJqXfc, https://www.youtube.com/watch?v=9rS8MVZH-vA
C3로 여러 작업을 해보는 시리즈: https://ebn.codeberg.page/programming/c3/c3-file-io/
프로젝트 예시로 Gameboy 에뮬레이터 https://github.com/OdnetninI/Gameboy-Emulator/, RISCV 베어메탈 Hello World https://www.youtube.com/watch?v=0iAJxx6Ok4E, roguelike "Depths of Daemonheim" https://github.com/TechnicalFowl/7DRL-2025가 있음
Tsoding의 C3 첫인상 스트림: https://www.youtube.com/watch?v=Qzw1m7PweXs - C3는 유망해 보이지만, null을 지원하는 언어라면 저런 계약 주석이 아니라 null 제한 타입이 필요함
모든 것을 null 검사하거나 그냥 YOLO로 넘겨야 한다면 Java를 쓰면 되고, Java조차 이 문제를 고치려 하고 있음: https://openjdk.org/jeps/8303099- 흥미로운 문제임. 원래는
int&를 null 불가 참조로,int를 포인터로 두는 식의&문법도 실험했는데, 거의 모든 포인터 매개변수는 null 불가가 되길 원하고, 생성자 없는 언어에서는 null 불가 변수를 맞추기가 매우 어렵다는 점이 드러남
ZII처럼 생성자/소멸자를 피하는 방식도 참조 값과 잘 맞지 않음. 결국 null 불가 타입은 할당되어야 하므로, 최초 할당 전까지 값이 준유효 상태로 존재하는 구간이 생김
C3에서도 범용적인 타입 안전 null 불가 타입을 만들 수는 있지만, 언어에 내장되어 있지는 않음 - Nim은 명시적이고 제한된 nullable 타입을 포인터와 참조 두 가지로만 둬서 이 문제를 해결함
포인터는 수동 관리, 참조는 자동 관리이며 둘 다 nil로 시작하고 참조 대상 객체를 직접 생성해야 함
나머지 언어는 스택 값과 스택 관리 숨은 고유 포인터를 쓰는 값 전달 기반이라, C나 C++ 라이브러리 인터페이스를 만들 때가 아니면 ref나 pointer를 거의 쓸 필요가 없음
4만 줄짜리 프로덕션 앱을 참조나 포인터 타입 없이 작성했음. 필요한 거의 모든 경우는 복합 타입이나 동적 컨테이너를 변경 가능한 값으로 넘기는 방식으로 처리되고, 포인터/참조 의미론을 수행할 수 없게 되어 있음. 수명은 이미 관리되므로 의미적으로는 그냥 값임 - 이런 함수 계약에는 아직 판단을 못 내리겠음. 다른 언어에서 10년 동안 봤지만 직접 써본 적은 거의 없어서 느낌을 말하기 어렵고, 다만 주석 안에 넣는 건 이상함
- 왜 문제를 푸는 방법이 하나뿐이어야 하는지 모르겠음
- 흥미로운 문제임. 원래는
- Rust를 몇몇 프로젝트에 써보고 나니 C3, Zig, Odin 같은 단순한 언어의 매력을 이해하게 됨
Zig 서브레딧의 한 표현이 적절했음: “Rust에 대한 내 지식을 디버깅하느라 시간을 다 쓰고 싶지 않고, 도구를 빨리 작성하고 디버깅하고 싶어서 내부 도구에 Zig를 썼다”- Zig가 지금 업무 프로젝트에 편하게 쓸 만큼 흔한가? 다음 사람이 짜증내며 다시 작성해야 하는 상황이 되지 않을까 싶음
Rust도 어느 정도 같은 문제가 있지만, 이제는 Rust 작성 자료가 많이 있음
- Zig가 지금 업무 프로젝트에 편하게 쓸 만큼 흔한가? 다음 사람이 짜증내며 다시 작성해야 하는 상황이 되지 않을까 싶음
- 이 비교를 보면 https://c3-lang.org/faq/compare-languages/ 최고의 C/C++ 대안 또는 진화형 언어는 D라고 볼 수도 있음
D에는 자체 크로스 플랫폼 GUI 라이브러리와 IDE도 있는데, 왜 널리 채택되지 못했는지 궁금함- 개인적으로는 D가 너무 크고, 아직도 상당 부분 가비지 컬렉션에 의존하기 때문이라고 봄
기능을 계속 추가하지만, 기능 추가가 언어를 쓸 만하게 만드는 건 아님. 사실 그건 C++에서도 가장 매력 없는 부분 중 하나임
추측하자면 D는 C++와 경쟁하려고 가비지 컬렉션에 베팅했지만 잘못된 선택이었고, traction을 얻지 못한 뒤 “언젠가 결정적 기능이 나올 것”처럼 기능을 계속 추가했으며, 그 추가 기능들이 오히려 매력을 떨어뜨린다는 점을 이해하지 못했음
이후 C++는 가비지 컬렉션 노선을 완전히 떠나 더 저수준 대안이 되었고, D는 고수준 대안처럼 느껴질 만큼 높지도 않고 C++와 경쟁할 만큼 낮지도 않은 애매한 위치가 됨
오래 존재했는데도 뜨지 못했다는 사실 자체가 이제는 “한물간 언어”처럼 보이게 만들어 더 뜨기 어려움
Walter Bright가 좋은 기능만 추린 D의 curated 버전을 만들 수도 있겠지만, 언어와 성숙한 표준 라이브러리를 만드는 데 걸리는 시간을 생각하면 말처럼 훨씬 쉽지 않음
- 개인적으로는 D가 너무 크고, 아직도 상당 부분 가비지 컬렉션에 의존하기 때문이라고 봄
- 흥미롭게도 C2도 있음: http://c2lang.org
- C4도 있지만, 그건 폭발물이거나 소프트웨어 아키텍처 모델링 표기법임
- 맞음, C3는 C2의 변형으로 시작했음
- C3와 Hare를 둘 다 써본 사람이 있는지 궁금함. 둘 사이에 겹치는 부분이 꽤 많아 보임
https://harelang.org/- Hare의 문제는 Linux/Unix 전용이고, 적어도 마지막으로 확인했을 때는 설계상 그랬다는 점임
그래서 많은 사람에게는 사실상 시작부터 제외됨 - C 대안 영역에는 Zig도 있음
https://ziglang.org/
- Hare의 문제는 Linux/Unix 전용이고, 적어도 마지막으로 확인했을 때는 설계상 그랬다는 점임
- 전체적으로는 그냥 그저 그래 보임
defer는 언어나 프레임워크에 적절한 기능이 없을 때 급히 흉내 낼 만한 것이지만, Python의with문이나 Java의 자동 리소스 관리가 더 낫다고 봄
Optional, Either 같은 것도 이제 넘어서야 함. 제대로 쓰려면 일이 많음. 1985년쯤 Byte 잡지의 CP/M용 터미널 에뮬레이터를 TRS-80 Color Computer의 OS-9으로 포팅하면서 C를 처음 접했는데, 정상 경로 10줄 정도가 오류 처리를 여기저기 엮느라 50줄로 불어나는 걸 보고 꽤 충격이었음
1995년에 Java를 봤을 때는catch {}로 수정하고finally {}로 보강할 수 있는 기본 실패 경로가 있어서 아주 반가웠음
예외가 멋없다고 생각할 수는 있지만, 그 정당화로 보이는 건 스택 추적을 디버깅용으로 채우는 일이 번거롭다는 점 정도임. 1990년대에는 예외가 실제로 작동하지 않던 C++ 명세의 여러 요소 중 하나였던 것도 맞음
오류 처리는 캡슐화에 대한 생각을 존중하지 않는다는 어려운 문제가 있지만 https://gen5.info/q/2008/08/27/what-do-you-do-when-youve-caught-an-exception/, 언어나 프레임워크가 해결할 수 있음에도 거의 다루지 않음
?, Optional, Either를 넣는 건 타이타닉의 갑판 의자를 재배치하는 것에 가까움- C3의 오류 처리는 꽤 새로움. 조합 가능성, 명시성, C 호환성 사이의 절충점을 찾으려 함
try-catch는 조합성이 좋음:try { int x = foo_may_fail(); int y = bar_may_fail(x); } catch (... ) { ... }
일반 Result 타입은 여기에flatmap이 필요하고, 오류 코드나 다중 반환도 이 부분에서 어려움
C3에서는int? x = foo_may_fail(); int? y = bar_may_fail(x); if (catch err = y) { ... return; }처럼 쓰고, 이후y는 암묵적으로int로 풀림
반드시 만족할 방식이라는 뜻은 아니지만, Optional이나 Either를 넘어서는 새 접근이고try-catch와도 공통점이 많음 - 솔직히 Python의
with가 뭘 하는지 아직도 잘 모름. 찾아보지 않고 말하자면, Python을 자주 쓰지 않아서 어떤 마법 같은 dunder 함수를 호출하는 것 같음
+,-같은 기본 연산도 사실은 dunder를 호출한다는 건 알지만, Python 문화에는 “그건 말하지 말고 모르는 척하게 해줘” 같은 분위기가 있음
숫자 비슷한 것이 아닌데 직접 dunder 함수를 작성하는 건 큰 코드 냄새이고, 숫자 비슷하더라도 잠재적 발화점일 가능성이 커서with는 늘 이상하게 느껴졌음
- C3의 오류 처리는 꽤 새로움. 조합 가능성, 명시성, C 호환성 사이의 절충점을 찾으려 함
- 작은 트집을 잡자면, 페이지 중간의
case문법에서 “빈break를 둘 수 없다”는 선택은 좋지만, 두 case가 같은 일을 하게 만드는 문법이case X: case Y:인 건 발화점이 될 수 있음
C3 작성자들에게는 겹쳐 쓰는 case를case X, Y:처럼 보이게 하라고 강하게 제안하고 싶음case X, Y는 값이 3~4개 정도일 때는 괜찮지만, 더 길어지면 문제가 쌓임
예를 들어case SOME_BAD_THING, SOME_OTHER_CONDITION, HERE_IS_NUMBER_THREE:뒤에 코드가 오는 형태는 읽기 어렵고, 다음 줄로 나눠도 가독성이 떨어짐
C에서는 enum에 대해 적어도default보다 완전한 switch를 선호해서, 이런 식의 fallthrough 목록이 10개 이상 되는 경우가 자주 있음
그래서case SOME_BAD_THING:,case SOME_OTHER_CONDITION:처럼 한 줄씩 두는 C 문법이 불일치에도 불구하고 더 명확하다고 봤음
case SOME_BAD_THING,처럼 각 줄에case와 쉼표를 섞는 절충안도 고려했지만, 결국 C 문법을 유지하는 편이 더 낫다고 느꼈음case 3,4:가 값 3 또는 4를 뜻하는 것과,case (3,4):가[3,4]값을 가진 배열을 뜻하는 것을 구분하는 건 미묘해 보임
- 아주 마음에 듦. 다만 이 문단은 산만했음
“매크로는 벌레 한 봉지다. 단백질 공급원으로 훌륭할 수는 있지만, 내가 정말 그걸 먹겠는가? 낚시할 때 벌레를 쓸 수는 있어도 집 안에서 쓸 일은 별로 없다. 비유를 벗어나 말하자면, 매크로는 틈새 사용 사례가 있고 그 용도에는 좋지만 남용되어서는 안 된다. 남용의 예로는 매크로를 지원하는 프로그래밍 언어 안에서 튜링 완전한 도메인 특화 언어를 만드는 것이 있다”