Ping 04/02/2023 .01: 씨언어의 장자 zig, rust, golang, c++ ???

Posted on Feb 4, 2023

최근에 재미로 zig 을 정말 재밌게 '읽었다'. 공부해보고 실제로 뭔가 코딩을 많이 해보거나 한 것은 아니고, 대체 어떻게 동작하는 프로그램을 이걸로 만들라고 하는건지 이해하고 싶었기 때문에 해봤다.

zig으로 프로그램을 짜는게 궁금하던 부분은 haskell의 monad을 이용한 방식, 혹은 apl이나 prolog으로 실제프로그램을 어떻게 만들 수 있을지 패러다임부터 다른 언어를 공부하고 생각해보는 것과는 조금 다를수도 있겠다. 어쨌든 그냥 imperative언어이니까.

오히려, zig을 어떻게 써먹어야 할지 궁금하던 지점은, c++의 스마트포인터와 move semantics등을 이해하는 것이나, rust의 borrowing checker, rc/arc등을 이해하는 것과 마찬가지인 것 같다. 둘 다 이해하고 익숙해지면 그렇게 부담스럽지 않을거 같긴 하다.

zig의 경우에는, rust의 빌림체킹 모델을 이해하고 익숙해지는 것과 유사한 종류의 궁금증이었지만, 정반대로 하는, unlearning의 경험이었던거 같다.

원한다면, 얼마든지 그런 comptime 라이브러리를 만들어서 shared_ptr, rc / arc 같은것들을 만들어 쓸수도 있을거 같다. 1

하지만, zig community에서는 이런 메모리관리 방식이 별로 그렇게 열심히들 사용하는것도, 정말 없으면 안된다고 중요하게 생각하는 것도 아닌거 같아서 놀랐다. c++/rust 커뮤니티와는 정말 다른 분위기, 후자는 정말 병적으로 느껴질 정도로 컴파일시간에, 컴파일러가 체크할 수 있어야 하고, 뭐든 안전하게 만들어야만 한다는 강박이 느껴진다. c++은 메모리관리 중요하고 안전하게 해야 하지만, 뭐 안그러고 적당히 잘 굴러가게 문제 없이 할 수 있다면 그거야 뭐 작성한 사람의 책임이지, 자유롭게 내버려두지, 약간 이런 분위기인거 같고. (물론 중요하다고는 말하지만)

오히려 그런 "smart pointer"보다는, 차라리 상황에 알맞게 더 적절한 allocator을 사용하는 것에 더 관심을 두는 것 같다는 인상을 받았다.

그리고 요즘의 유행하던 대다수의 언어들, Java, TypeScript, 같은 Generics의 지원이 흥미로웠다.

comptime 을 이용해서, 실제로는 c++ template이 특정한 타입의 코드를 컴파일 시점에 생성해내는 방식을 직접 활용할 수 있도록 한 것이 재밌었다.

그러면서도, c++ template보다는 더 단순해 보이고 해서 마음에 들기도 했다.

최근에는 golang에도 generics이 추가 되어서 반가웠다. go을 사용할 때엔, for-loop으로 타입마다 매번 뭔가 알고리즘을 복붙하듯이 짜거나, go generate 에 의존해야 하는 것이 귀찮았었기 때문에.

그리고 comptime이 흥미로웠던 이유는, common-lisp의 타입시스템과 유사한 방식이기도 해서였다. 커먼리습은 실행시간과 컴파일시간, 파싱시간을 분리해놓았는데, 그 시점들을 모두 내가 작성한 코드을 통해 제어가 가능한데, satisfies 등을 보면, 타입시스템 자체는 제네릭에 대한 고려가 별로 없지만, 그렇게 동작하도록 얼마든지 지정이 가능하게 되어 있기 때문이다. 그러면서도 딱히 그렇게 엄청나게 복잡하거나 hacky하게 해야만 하는 것도 아니어서 만족스러운 접근.

java이나 typescript등의 제네릭은, 그리고 아마도 golang도, 모든 것을 컴파일러가 딱 정해진 범위의 타입시스템에 따라서만 동작하는데, 그걸 계속해서 더 편리해지도록 지원을 추가해 나가는 느낌이다.

아마 java은 더 확장하지 않을 것 같고 (적어도 제네릭에 대해서는), golang도 그럴거 같지만, typescript은 확실히 매 버젼이 나올 때마다, 더 간략하게 타입시그니쳐를 명확하게 표기하거나 하는 지원을 추가해 나가는 것 같다.

zig와 유사하게, lua의 문법인데, static type system을 붙이고, AOT compile방식으로 동작하는 nelua을 보면, 이런 컴파일시점을 이용하여 타입시스템을 확장할 수 있도록 해놓았다.

nelua의 매크로는 노골적으로 Lisp/Scheme의 매크로시스템을 연상시키는 방식이다. (리습 매크로의 gensym 이나 AST을 조작도 아예 가능하게 해놓았다)

nelua의 매크로와 비교하면, zig의 comptime은 오히려 ocaml의 functors와 유사한 방식으로 사용하게 될거 같다.

zig의 comptime, 커먼리습의 타입시스템과 satisfies, nelua의 macro등은 모두 조금씩은 다르지만, 자바나 타입스크립트의 generics을 넘어서는 응용도 가능할거 같다: 예를 들면, https://en.wikipedia.org/wiki/Dependent_type 이런 타입시스템을 추가적으로 지원하도록 확장도 손 쉬울거 같다. 2

malloc/free 와 유사한 방식의 allocators 방식이거나, 한것도 그렇고 어떻게 생각하면 zig은 괜찮은 방식의 comptime을 지원하는 씨언어처럼 느껴지기도 한다.

하지만, 가장 불만족스러웠던 부분은 컴파일속도인거 같다.

어차피 rust처럼 복잡하게 컴파일시점 체크가 많거나, 최적화도 열심히 해주듯이, comptime 때문인지 llvm백엔드를 이용하기 때문인지, 그냥 씨언어만큼 가볍게 컴파일되는 느낌은 아니었어서.

컴파일시간이 쾌적한건, 사실 그냥 씨언어나 golang까지만인거 같아. 그리고 두 언어의 killer-feature에 간과하기 쉬운 점이지만, 빠르고 상대적으로 쾌적한 컴파일속도를 꼽아야 한다고 나는 생각해서.

단순하게만 생각하면 c++이 씨언어의 장자인 것 같지만, 실제로 컴파일속도, abi호환 등을 생각해보면 사실 완전히 다른 언어에 가까운거 같다. (물론 그럼에도 다른 어떤 언어들보다 씨언어와 통합해 쓰기 가장 편리하고 오버헤드도 없지만)

개인적으로는 rust보다는 cppfront이 오히려 더 기대되기도 한다. 그냥 c++처럼 적당히 동작하길 바라고, 적당히 borrowing-checker이 붙거나 하는 자유도 기대할 수 있을거 같아서. (물론 현실은 rustlang을 무시할 수 있게 될거 같진 않다. 이미 가속과 질량이 엄청 붙어버렸으니까.)

zig은 그 자체가 씨언어 컴파일러이기도 하지만, 컴파일속도면에선 쾌적하지 못한거 같아서 내겐 애매했다.

golang은 뜬금 없이, 가비지컬렉터도 붙어 있고, 생성된 바이너리가 씨언어와 호환도 없을거고, 심지어 씨언어 바이너리를 abi으로 호출할 때에 오버헤드까지 있다고 하는거 같아. …그런데 오히려 내게는 씨언어의 정신적인 장자가 맞지 않을까 싶다. 컴파일속도에 대한 고려와 언어의 단순함 등등을 제한한 점 등일거 같아. (..물론 씨언어가 그런 면들을 처음부터 추구했다고 할수는 없을거 같지만)

딱히 결론을 낼 생각은 없지만, 어중간한 느낌이 드는건 어쩔 수 없는거 같다.

Footnotes


1

실제로 comptime으로 https://github.com/yrashk/zig-rcsp 에서, c++의 std::shared_ptr 같은 reference-counted shared pointer을 구현.

2

이미 커먼리습의 타입시스템은 얼마든지 어떤 값에 따라 타입이 결정되도록 하는 것이 기본이니까.