3P by GN⁺ | ★ favorite | 댓글 1개
  • C3는 C와 함께 쓰는 시스템 언어를 목표로 하며, 모듈 시스템, 제네릭, 컴파일 타임 실행, 오류 처리, defer, 슬라이스, foreach 같은 기능으로 C 개발자의 전환을 쉽게 만듦
  • 문법은 C에 가깝지만 fn, foreach, defer, implicit break, nextcase, 타입 메서드, 계약(contracts)처럼 표현력과 안전성을 보강하는 장치가 많음
  • Gentoo에서 소스 빌드를 시도할 때 LLD 라이브러리 탐지 문제가 있었고, 결국 사전 빌드된 c3c 0.7.1c3c 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는 슬라이스 값을 순회하며, 변수명 앞에 &를 붙이면 참조 기반 반복이 가능함
    • breakcontinue는 예상한 방식으로 동작함
    • 일반 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에서 흔히 쓰는 함수 끝의 goto cleanup 블록보다 훨씬 낫다고 평가함
  • errdeferokdefer에 해당하는 동작은 각각 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를 가짐
    • faultdefOOPS, 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 패키지를 찾지 못해 소스 빌드를 시도함
  • 빌드 과정에서 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, LLVM 17.0.6, target x86_64-pc-linux-gnu로 표시됨
  • c3c init hello_world는 프로젝트 디렉터리와 LICENSE, README.md, project.json, src/main.c3 등을 생성함
    • LICENSEREADME.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에는 기본 할당자로 tmemmem이 있으며, tmem은 arena allocator처럼 동작하는 임시 할당자
    • @pool() 범위 안에서 tmem으로 할당한 항목은 스코프 종료 시 해제됨
    • 표준 라이브러리의 여러 함수는 t 접두사로 임시 할당자 사용을 나타냄
  • 이 방식은 Zig처럼 allocator를 명시적으로 넘기는 관행과 C처럼 표준 allocator를 쓰는 관행의 중간에 가까움
  • mainint를 반환하면 C3는 끝에서 값을 반환하도록 요구함
    • 반환이 빠지면 “Missing return statement at the end of the function” 오류가 발생함
    • mainvoid로 표시하는 것도 가능함
  • io::printfio::printfn은 반환값을 반드시 처리하지 않아도 되며, 이는 @maydiscard annotation 때문임
    • 반대 annotation으로 @nodiscard도 있음

토크나이저 구현에서 본 장단점

  • 토크나이저는 입력 문자열을 Token[]로 변환하며, TokenTag enum으로 ADD, SUBTRACT, MULTIPLY, DIVIDE, POWER, 괄호, NUMBER, DELIMITER를 정의함
  • 숫자는 문자열에서 숫자와 .을 스캔한 뒤 to_float()로 변환함
  • 표준 라이브러리의 std::collections::listList {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[] sourceusz 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()! 같은 재전파 덕분에 오류가 없는 코드처럼 작성하면서 잘못된 입력은 바깥으로 전파됨
  • nextcaseexpressionterm에서 반복적인 +, -, *, / 처리를 깔끔하게 만드는 데 잘 맞았음
  • 예제 입력 2+3 * (-7/-4) ^ 3.14Result: 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 작성 자료가 많이 있음
  • 이 비교를 보면 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 버전을 만들 수도 있겠지만, 언어와 성숙한 표준 라이브러리를 만드는 데 걸리는 시간을 생각하면 말처럼 훨씬 쉽지 않음
  • 흥미롭게도 C2도 있음: http://c2lang.org
    • C4도 있지만, 그건 폭발물이거나 소프트웨어 아키텍처 모델링 표기법임
    • 맞음, C3는 C2의 변형으로 시작했음
  • C3와 Hare를 둘 다 써본 사람이 있는지 궁금함. 둘 사이에 겹치는 부분이 꽤 많아 보임
    https://harelang.org/
    • Hare의 문제는 Linux/Unix 전용이고, 적어도 마지막으로 확인했을 때는 설계상 그랬다는 점임
      그래서 많은 사람에게는 사실상 시작부터 제외됨
    • C 대안 영역에는 Zig도 있음
      https://ziglang.org/
  • 전체적으로는 그냥 그저 그래 보임
    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는 늘 이상하게 느껴졌음
  • 작은 트집을 잡자면, 페이지 중간의 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] 값을 가진 배열을 뜻하는 것을 구분하는 건 미묘해 보임
  • 아주 마음에 듦. 다만 이 문단은 산만했음
    “매크로는 벌레 한 봉지다. 단백질 공급원으로 훌륭할 수는 있지만, 내가 정말 그걸 먹겠는가? 낚시할 때 벌레를 쓸 수는 있어도 집 안에서 쓸 일은 별로 없다. 비유를 벗어나 말하자면, 매크로는 틈새 사용 사례가 있고 그 용도에는 좋지만 남용되어서는 안 된다. 남용의 예로는 매크로를 지원하는 프로그래밍 언어 안에서 튜링 완전한 도메인 특화 언어를 만드는 것이 있다”