내가 OCaml을 주 언어로 선택하게 된 이유와 언어 자체, 생태계, 커뮤니티의 장점, 그리고 흔한 오해들에 대한 생각을 정리한 글.
이 의견글 에서 나는 내가 이 언어를 어떻게 만나게 되었는지 짧게 공유하고, 그 장점들을 언어 자체, 그 생태계, 그리고 그 커뮤니티를 다루는 여러 섹션으로 나누어 정리해 보려 한다. 또한 인터넷에서 흔히 보이는 몇몇 유명한 신화들(또는 오해들)을 반박 해보려고도 한다. 투명성을 위해 밝히자면, 이 글을 쓰는 시점에 나의 직업적 일은 OCaml 생태계를 위해, 그리고 그 위에서 일하는 것 과 관련되어 있다. 다만 몇 년 동안 나를 지켜본 독자들이라면, 내가 OCaml 생태계에서 일하고 돈을 받기 훨씬 전부터 이 언어를 홍보해 왔다는 것을, 때로는 다소 과하게 해왔다는 것을 증언해 줄 수 있을 것이다.
우선, 이 글은 왜 내가 개인적으로 OCaml이 많은 맥락에서 적절한 선택이라고 믿는지를 설명할 것이다. 내 목표는 꼭 여러분을 설득하는 것은 아니다—물론 그렇게 된다면 매우 반가운 부수 효과 이겠지만—그리고 내가 제시하는 많은 논거는 다른 언어들에도 똑같이 적용될 가능성이 크다!
또한 아주 자주, 새로운 언어를 탐험하고 싶어 하거나 OCaml로 작성된 해결책을 시도해 보려는 사람들에게 내가 OCaml을 권하면, 친절하게도 나는 늘 OCaml을 홍보한다 는 말을 듣곤 한다. 흥미로운 점은, JavaScript처럼 기본값으로 채택된 언어들이나 Rust 또는 Go 같은 더 최근의 언어들을 제안할 때는 이런 반응이 덜하다는 것이다. 아마 사람들은 암묵적으로 덜 알려진 언어를 제안하는 것은 비합리성과 개인적 취향 쪽으로 기운다고 여기기 때문일 것이다. 내 관점에서는, 세밀한 메모리 제어가 필요하지 않은 많은 경우에 OCaml을 제안하는 것은 Rust를 제안하는 것만큼 적절하며 (아마 그보다 더 적절할 수도 있다).
이 서문을 마무리하자면, 많은 사람들은 학부 과정이나 준비반에서 처음 OCaml(또는 Caml Light)을 접했고, 종종 산업과는 거리가 먼 맥락에서 사용했다. 반면 나는 훨씬 더 일찍 Site du Zéro 덕분에 OCaml에 관심을 갖기 시작했다. 그곳에는 함수형 프로그래밍 애호가들의 작은 커뮤니티가 있었고, OCaml, Erlang, Haskell 같은 덜 주류적인 언어들을 홍보하고 있었다. 대학에서 OCaml과 상호작용한 것은 내게 그저 덤 이었다.
OCaml을 선택하는 이유를 기록한 사람이 내가 처음은 아니다. 내 생각에 살펴볼 가치가 있는 다른 자료들도 많고, 그것들은 OCaml 사용자들이 대체로 매우 만족하고 있음을 보여준다 — 그래서 어떻게 그리고 왜 우리가 이 언어를 주 기술로 선택했는지를 공유하고 싶어 할 정도로 말이다.
"왜 OCaml인가?", Real World OCaml 책의 프롤로그로, OCaml 사용의 사실적인 장점을 소개한다(그리고 도입부에는 타임라인도 포함되어 있다). 이 책은 여러 면에서 훌륭하지만, 나는 그 사용 방식이 꽤 편향되어 있다고 느끼기 때문에 추천하지 않는 습관이 생겼다. 커뮤니티에서 반드시 널리 받아들여진 것은 아닌 라이브러리들을 기본 선택처럼 제안하기 때문이다.
"Better Programming Through OCaml", OCaml Programming: Correct + Efficient + Beautiful 책(영상도 함께 제공된다)의 프롤로그로, 주로 OCaml을 배우는 것이 어떻게 다른 더 대중적인 기술들에서 개발자의 역량을 향상시킬 수 있는지 설명한다. 이 책은 꽤 최근의 것이고, 지금 내가 OCaml 입문용 대표 자료로 추천하는 책 이다.
강연: "Why OCaml?", Jane Street의 CTO이자 금융 분야의 세계적 선도 기업에서 OCaml을 산업적으로 사용하는 Yaron Minsky의 발표다. Yaron은 Real World OCaml 의 공동 저자이기도 하고, 정적 타입 프로그래밍 언어 세계에서 널리 인용되는 표현인 "Make illegal states unrepresentable"의 발안자이기도 하다. 이 강연은 Jane Street가 왜 OCaml을 선택했는지에 대한 통찰을 풍부하게 제공한다.
"OCaml for Fun & Profit: An Experience Report", Yow 2023에서 Tim McGilchrist가 발표한 강연이다. 언어에 대한 풍부한 소개 후, 재미와 수익을 함께 한 OCaml의 실제 프로덕션 사용 사례들을 다룬다.
Thomas Leonard의 "Replacing Python for 0Install". 이 연재 글들은 내 생각에 엄청나게 흥미롭다. 분산형 크로스플랫폼 소프트웨어 설치 시스템인 0Install의 저자는(Nix보다 약간 오래된 대안), 새 버전 구현을 위해 Python 외의 언어를 찾고 있었고(Python을 대체하려는 이유 역시 여기에 문서화되어 있다), 여러 후보를 철저하고 체계적으로 비교했다: ATS, C#, Haskell, Go, Rust, OCaml, 그리고 Python. 여러 해가 지난 지금도 나는 이 연재의 엄밀함과 균형감에 감탄하고 있으며, 강력히 추천한다.
아마 다른 자료와 증언들도 있을 것이다. 특히 공식 웹사이트에는 산업 및 학계 사례 연구가 소개되어 있다. 물론 OCaml이 줄 수 있는 좌절감을 표현한 글들도 있다. 나는 OCaml이 완벽하지 않다는 것을 알고 있고—어떤 기술도 완벽하다고 믿지 않는다. 아마 신화들 섹션과 결론에서 이런 글들을 (암묵적으로든 명시적으로든) 몇몇 언급하게 될 것이다. 거기서 나는 어떤 맥락에서는 OCaml이 적절한 선택이 아니라고 생각하는지도 설명해 보려 한다.
언어가 제공하는 기능들 에 들어가기 전에, 내가 본질적이라고 생각하는 한 가지 점부터 시작하고 싶다. OCaml은 연구에서 출발했고 산업 사용자들에 의해 사용되는 프로그래밍 언어다. 이 이중성은 중요하다. 왜냐하면 그것이 이 언어에 두 가지 핵심 장점을 제공하기 때문이다.
흥미로운 언어 개념으로서 바람직한 기능들에 대한 지침을 주며, 이는 첨단 연구에 의해 뒷받침된다. 예를 들어 내 아는 한, OCaml은 사용자 정의 effect를 네이티브로 지원하는 최초의 주류적인 언어이며, 이는 수많은 논문으로 드러나는 최첨단 연구의 결과다.
산업화 도구로서 바람직한 기능들에 대한 지침 역시 제공하며, 이 또한 연구에 의해 뒷받침되고 실제 사용 사례에 의해 동기화된다. 예를 들어 최근 Jane Street는 중요한 산업용 OCaml 사용자로서, 선형 자원 관리를 가능하게 하는 affine sessions 의 통합을 제안했다(다소 Rust와 유사한 방향이다).
산업적 동기와 학문적 동기가 이렇게 얽혀 있기 때문에 OCaml은 견고하고 유용하며 잘 정의된 기능들의 집합을 제공할 수 있다. 다시 말해, OCaml은 살아 있는 언어이며, 내가 이 언어를 사용해 오면서 나는 수많은 발전과 추가 사항들을 목격했다. 그리고 그것들은 OCaml에 대한 흔한 주장, 즉 이 언어는 이론이나 Coq/Rocq 구현에만 유용하다 는 말을 반박 한다.
역사적으로는 그 말이 사실이었을 수 있지만, 산업 사용자들이 제공한 동기 덕분에 "표현력과 안전성에 중점을 둔 산업 수준의 함수형 프로그래밍 언어"라는 표어는 정당성을 갖는다. 25 Years Of OCaml이라는 제목의 Xavier Leroy의 OCaml Workshop 2021 개막 기조연설은, OCaml의 지속적인 설계에 대한 방대한 타임라인을 제시하며 언어가 거쳐 온 여러 진화 단계를 보여준다.
대체로 말하면, OCaml은 ML 계열의 프로그래밍 언어로, 고수준 (여기서는 가비지 컬렉션을 갖춘다는 의미), 정적 타입 (암묵적 변환 없이 컴파일 시점에 타입을 검사함), 그리고 타입 추론 (또는 type synthesis)을 제공한다. 따라서 컴파일러는 대부분의 경우 표현식의 타입을 스스로 추론할 수 있다. 이것은 함수형 스타일과 명령형 스타일 모두로 프로그래밍할 수 있게 해준다.
OCaml은 또한 객체 지향 프로그래밍 모델 과 매우 풍부한 모듈 시스템 을 제공한다. 이 언어에는 두 가지 컴파일 방식이 있다: 가상 머신 에서 실행 가능한 바이트코드 로 컴파일하는 ocamlc(이식 가능하고 효율적), 그리고 네이티브 기계 코드 로 컴파일하는 ocamlopt(넓은 아키텍처 범위에서 실행 가능).
게다가 OCaml은 Js_of_ocaml을 사용해 바이트코드를 JavaScript로 변환 할 수 있어, OCaml 생태계 안에서 매우 빠른 상호운용을 가능하게 한다(이 웹사이트에서 내가 광범위하게 사용하는 방식이다). 같은 접근은 WebAssembly를 생성하는 데에도 사용된다. JavaScript 생태계와 더 깊이 상호운용하려면, Melange가 Js_of_ocaml과는 다소 다른 접근으로 견고한 JavaScript를 생성한다.
OCaml은 매우 다재다능한 언어이며, 이제부터 나는 왜 이 언어가 — 나에게 — 개인 프로젝트와 직업 프로젝트를 모두 만드는 데 이상적인 도구인지, 그 기능과 강점을 소개해 보려 한다. 먼저 정적 타입에 대한 짧은 우회로부터 시작하겠다.
Bruno와 함께 OCaml에 посвящ된 If This Then Dev 에피소드를 준비할 때 — 결국 Didier와 함께 녹음되었다 — 그는 내게 놀라운 질문을 했다.
“개인 프로젝트를 빨리 진행할 때도 굳이 타입에 신경 쓸 가치가 있을까? 프로덕션 코드에서의 가치는 충분히 알겠지만, 개인 프로젝트 에서는 시간 낭비처럼 보여.”
나는 여기에 답하는 두 가지 큰 관점이 있다고 생각한다. 첫 번째이자 가장 분명한 것은, 원칙적으로 나는 왜 개인 프로젝트가 직업 프로젝트보다 덜 엄격해야 하는지 모르겠다 는 점이다. 내가 나 자신을 위해 소프트웨어를 쓸 때, 구현의 코너 케이스 를 무시해도 될 수는 있다. 물론 가능하다. 하지만 아마 내가 정말로 원하는 것은 그게 아닐 것이다. 그러니 언어와 컴파일러가 내 소프트웨어의 모든 경우를 고려하도록 강제하는 안전망을 제공해 준다면, 나는 그것을 사용한다 — 마치 unit tests 를 작성하는 것이 개발을 더 쉽게 만들어 주듯이, 나는 그것을 제약으로 보지 않는다.
하지만 개인 프로젝트의 위생 문제를 넘어, 정적 타입 검사가 나쁜 평판을 갖게 되는 이유는 보통 좋지 않은 경험 때문이다. 실제로 C나 Java 같은 언어에서 타입은 대체로 제약 일 뿐이고 쉽게 우회될 수 있다. OCaml, Haskell, F#, Scala, Rust처럼 타입에 강한 비중을 두는 언어들에서는 타입이 안전장치 역할 을 한다. 더 중요한 것은, 내 생각에 타입은 표현력 있는 설계 도구이기도 하다 는 점이다. 타입을 사용하면 안전성을 확보하면서도 데이터를 설명하는 엄청나게 풍부하고 유연하며 간결한 방법을 얻게 된다.
내 경험상, 허술하게 타입이 붙은 언어에서 — 미안하지만 이 유혹은 너무 강하다 — 동적 타입 언어로 옮겨가는 것은 흔한 일이다. 예를 들어 나도 Java에서 Ruby로 즐겁게 옮겨갔다. 하지만 OCaml이나 Haskell처럼 풍부한 타입 시스템을 가진 언어에서 출발하면 동적 타입 언어로의 전환은 훨씬 더 어려워진다. 현재로서는 OCaml이나 Haskell 같은 언어를 진지하게 사용해 본 뒤, 더 단순한 타입 시스템을 가진 언어로 기꺼이 돌아간 사람을 나는 알지 못한다 (물론 흥미로운 프로젝트가 그런 기술적 퇴행을 정당화할 때는 있다).
이것은 단지 개인적인 관찰이 아니다. 정적 타입 검사는 프로그래밍 언어의 진화에 관한 더 넓은 논쟁의 중심에 있다. 역사적인 언어들 역시 더 많은 타입 검사를 통합하도록 진화하고 있다(또는 진화하려 하고 있다). 예를 들어 Erlang은 1980년대에 이미(컴파일러 소스가 공개되기 전부터) 타입 시스템 통합을 실험했다. Java도 버전이 올라갈 때마다 sealed families 같은 정적 타입 검증을 개선하는 기능들을 강화하고 있다.
수많은 언어들이 타입 시스템을 실험하고 있다: RBS를 갖춘 Ruby, Crystal(Ruby에서 강하게 영감을 받은 정적 타입 언어), Mypy를 쓰는 Python, Elixir(Erlang의 과거 실험을 다시 다루며 점진적 타이핑에 대한 현실적인 접근을 제시함), 그리고 물론 JavaScript 커뮤니티에서 널리 채택된 TypeScript가 있다.
이 모든 시도는 고무적이고 분명 올바른 방향으로 가고 있지만, 현재로서는 주로 안전장치를 추가하는 역할 을 할 뿐, 아직은 표현력 있는 설계 도구 의 역할까지는 하지 못하고 있다.
점점 더 풍부해지는 타입 시스템의 중요성과 관련해, 미국 백악관 은 최근 보고서를 발표하여 소프트웨어 설계에서 메모리 안전성 의 중요성을 강조했고, 나아가 C++보다 Rust 언어(역사적으로 OCaml로 작성되었다가 이후 self-hosted 가 된)를 권장 했다. 이는 심지어 공식 기관들(흔히 시대에 뒤처졌다고 여겨지는)도 풍부한 타입 시스템의 가치를 강조하고 있음을 명확히 보여준다. 더욱이 이 글을 쓰는 시점에 내가 일하고 있는 회사가 내놓은 Tarides의 응답도, 중요한 시스템을 구축할 때 OCaml 사용을 지지하는 설득력 있는 논거를 제시한다.
결론적으로, 정적 타입 검사는 정말로 가치가 크고 적극 권장할 만하며, OCaml 같은 정교한 타입 시스템을 가진 언어들을 탐구해 보고, 나아가 점점 더 형식 기법들로 들어가 보는 것도 충분히 해볼 만한 일이다.
거대한 OCaml 튜토리얼을 만들고 싶은 유혹이 아주 크지만, 이 섹션의 목적은 왜 OCaml 이 나에게 학습과 실전 모두에서 매우 적절한 선택인지 보여주는 데 있다. 따라서 장점들은 제시되고(그리고 옹호되고), 하지만 이것은 튜토리얼이 아니다.
오늘날 멀티 패러다임 언어를 말하는 것이 불필요해 보일 수도 있다. 왜냐하면 산업이 선호하는 대다수 프로그래밍 언어는 이미 멀티 패러다임이기 때문이다. 그러나 OCaml은 함수형 프로그래밍 언어이면서 명령형 프로그래밍, 모듈형 프로그래밍, 객체 지향 프로그래밍, 그리고 5.0.0 버전부터는 멀티코어 프로그래밍 도 지원한다.
Haskell이 함수형 프로그래밍 세계에서 널리 인정받는 것처럼, 어떤 이들은 언어에 명령형 메커니즘을 추가하는 것이 나쁜 생각이라고 여기곤 한다 — 특히 함수형 스타일의 이점을 굳게 믿는 경우라면 더 그렇다. 내 관점에서는, 언어가 허용하는 한 명령형 프로그래밍을 사용하는 데는 완전히 정당한 이유가 몇 가지 있다.
구현의 가독성. 때로는 가변성을 피하려다가 추가적인 배관 작업(예를 들어 State Monad)이 필요해지고, 이것이 프로그램을 읽고 이해하는 일을 더 번거롭게 만들 수 있다.
성능. 이런 배관을 추가하면 오버헤드가 생겨 구현 실행 비용이 더 커질 수 있다.
사용의 용이성. 몇 년 전 Arthur Guillon은 아주 엄숙하게 내게 "OCaml은 effect를 자명하게 허용하는 람다 계산이다" 라고 말한 적이 있다. 이 점은 표준 출력에 메시지를 간단히 찍는 방식의 디버깅 같은 작업에서 매우 효과적이다. 물론 이것이 로깅을 구현하는 최선의 방식 은 아닐 수 있다는 점은 인정하지만, 부정할 수 없이 편안한 사용자 경험을 제공하고 빠른 프로토타이핑을 가능하게 한다.
일반적으로 OCaml의 이중적 성격 — 명령형이면서 함수형인 성격 — 은 서로 다른 상황에서 두 패러다임의 장점을 활용하게 해 주고, 물론 둘을 조합할 수도 있게 한다. 예를 들어 모듈의 명령형 성격을 함수형 API 뒤에 숨기는 식이다.
문법은 종종 사소한 세부 사항으로 여겨지지만, ML 계열 언어들은 간결하고 표현력이 높으며 읽기 쉬운 문법을 갖고 있다. 물론 이 문법 계열 은 더 전통적인 C 스타일 문법에서 온 사람들에게는 혼란스러울 수 있지만, 꽤 빨리 익숙해질 수 있고 곧 이 문법이 매우 일관되고 상대적으로 모호하지 않다는 점을 깨닫게 된다. 다만 OCaml의 문법이 여러분에게 문제라면, 중괄호를 사용하는 대체 문법인 ReasonML도 살펴보길 권한다.
OCaml은 Caml의 역사에서 드러나듯 프랑스 연구에서 출발한 언어이며, 주로 증명 보조기 Coq/Rocq를 구현하기 위해 설계되었다. 이 출발점 — 그리고 Coq를 구현하면서 동시에 준비반에서 가르치는 프로그래밍 언어로도 쓰이게 하려는 초기 동기 — 는 일종의 이중성을 만들어 낸다.
핵심 기능들은 처음부터 산업을 염두에 두고 설계된 것은 아니었다. 하지만 이 주장은 더 이상 사실이 아니다. 주된 이유는 OCaml이 이미 산업 현장에서 사용되는 언어가 되었기 때문이다. 언어의 탄생기에는 "기업용" 애플리케이션을 만드는 도구보다 언어 그 자체를 만들기 위한 도구들(컴파일러 메커니즘 교육을 돕는 도구들)이 더 많았지만, 산업적 사용에 동기화된 커뮤니티 프로젝트들이 언어와 생태계를 풍부하게 만들면서 산업에도 적합한 다목적 도구가 되었다. 예를 들어 Tk 라이브러리와의 binding 을 만들려는 시도는 named arguments, optional arguments, 그리고 polymorphic variants를 언어에 통합하게 만들었다.
패러다임과 언어 기능들의 집합은 신중하게 설계되고 잘 이론화되어 있다. 일반적으로 어떤 기능(또는 기능 집합)의 통합은 견고한 이론적 기반에 바탕을 둔 치밀한 연구의 결과이며, 해당 분야의 수많은 전문가들(종종 과학계에서 공인된)에 의해 검토된다. 이런 엄밀함은 때때로 새로운 기능 도입을 늦출 수 있지만, 대체로 그 기능들의 올바른 동작과 이론적 안정성을 보장한다.
연구 세계와의 명백한 근접성에서 비롯된 이러한 이론적 엄밀함 덕분에, OCaml의 여러 측면들은 잘 문서화되어 있고 방대한 수의 논문으로 뒷받침되며 예측 가능한 동작 을 보인다. 내 관점에서 이것은 이런 다양한 기능들을 깊이 있게 이해하기 위한 매우 현명한 선택으로 OCaml을 만들어 준다. 예를 들어 나는 OCaml 덕분에 프로그래밍 언어의 어떤 특성이나 패러다임들을 훨씬 더 잘 이해하게 되었다 고 믿는다.
더 나아가, 치밀하고 엄격한 연구가 어떻게 언어 기능의 통합을 뒷받침할 수 있는지를 보여주는 좋은 사례가 바로 OCaml의 객체 모델 구현이다. Jérôme Vouillon의 박사논문인 Design and Implementation of an Extension of the ML Language with Objects 는, 상속과 서브타이핑의 개념을 분리함으로써 — 상속은 구문적 개념 이고 서브타이핑은 의미적 개념 이다 — 타입 추론과 아주 잘 결합되는 혁신적인 객체 모델을 제안한다. 이 모델은 row polymorphism을 사용해 구조적 서브타이핑 관계를 설명하며, 이는 Java, C#, 그리고 대부분의 대중적인 OOP 언어들이 쓰는 명목적 서브타이핑과 대조된다. OCaml의 객체 모델은 어떤 추가적인 형식 절차 없이도 SOLID 원칙을 완전히 충족한다.
정적 타입 검사가 있는 언어를 내가 왜 가치 있게 여기는지에 대해 꽤 길게 이야기했다. 하지만 내 경험상, 정적 타입 언어가 정말로 실용적이려면 대수적 타입이 필요하다.
곱 타입: 서로 다른 타입의 값들을 묶을 수 있게 해준다(즉, 이질적 타입들의 논리곱 을 만든다). 보통 모든 주류 언어에 존재한다(예를 들어 추가 개념을 도입하는 객체, 혹은 튜플과 레코드).
합 타입: 서로 다른 값 타입들의 논리합 을 만들 수 있게 해주며, 각기 다른 case 들이 생성자에 의해 구분된다. booleans 처럼 합 타입의 몇몇 특수한 경우 는 주류 언어들에도 있다(true 와 false, 즉 매개변수 없는 두 생성자의 논리합). 하지만 완전한 합 타입 지원은 대중 언어들에서 종종 불편하다. 예를 들어 Kotlin과 Java(그리고 사실상 C#)는 상속 관계와 결합된 sealing이라는 구성을 사용한다. 전용 합 타입 문법도 Scala에 비교적 늦게 들어왔고, 그 이전 버전들은 sealed families에 의존했기 때문에 합을 표현하는 것이 장황하고, 내 생각에는 추론하기도 더 어려웠다.
지수 타입: 고차 함수(인자로 전달되거나 결과로 반환될 수 있는 함수)들의 타입을 표현하는 함수들을 기술할 수 있게 해준다.
pattern matching, parametric polymorphism(또는 generics)과 결합되면, 대수적 타입 시스템은 데이터 구조, 프로그램의 상태 기계, 혹은 적절한 카디널리티를 지닌 비즈니스 도메인을 모델링하기 위한 엄청나게 표현력 있는 도구가 된다. 21세기인 지금도, 곱 타입과 지수 타입이 흔한 세상에서도, 내가 아주 인기 있는 언어들을 사용할 때 자주 느끼는 좌절은 합 타입의 부재다. 그 때문에 장황한 인코딩을 사용해야 하고(도메인의 카디널리티를 증가시킴), 이 문제는 특히 Go와 TypeScript를 다룰 때 두드러진다.
이 삼박자의 매력은, 사실 Rust의 성공 뒤에 있는 이유 중 하나일 가능성이 높다(매우 인체공학적인 생태계와 도구 체계도 함께 작용했겠지만). 요컨대, 만약 여러분이 정적 타입 검사를 갖춘 새로운 프로그래밍 언어를 만들 생각이라면, 제발, 대수적 타입을 포함시키는 것을 주저하지 말길 바란다!
마지막으로, OCaml의 타입 시스템 가운데 여기서 다루지 않은 측면들도 있는데, 아마 별도의 글이 필요할 것이다. 예를 들어 훨씬 더 많은 불변성을 표현할 수 있게 해주는 generalized algebraic data types (GADTs)가 있다.
OCaml은 그 조상인 Caml Light를 통해, Standard ML과 유사한 모듈 시스템을 제공한 최초의 언어들 가운데 하나였다. 이 시스템은 Modula-2 스타일로 캡슐화와 추상화 를 제공하면서도 분리 컴파일 을 지원한다. OCaml의 모듈 시스템은 언어의 근본적인 측면 이지만, 그 복잡성은 위협적으로 느껴질 수 있다. 실제로 OCaml에서는 인터페이스(signature)와 구현(structure)을 명확히 구분할 수 있어서 캡슐화와 문서화를 용이하게 하고, 동시에 모듈 언어 안에서 함수 적용 도 가능하게 한다.
모듈이라는 주제를 짧게 다루는 것은 내게 특히 어렵다(이건 내가 몇 년째 블로그에서 탐구하고 싶어 하던 주제다). 그래도 OCaml의 매우 모듈적인 접근이 가진 장점들을 몇 가지 적어보면 다음과 같다.
분리 컴파일: 큰 프로그램을 효율적으로 컴파일하기 위해 병렬 및 증분 컴파일을 최적화할 수 있는 결합 지점을 식별하게 해주는 핵심 기능이다. 이 접근은 OCaml의 권장 빌드 시스템인 dune에서 활용된다.
구현과 인터페이스의 체계적 분리: 캡슐화, 그리고 문서를 인터페이스에 배치할 수 있다는 점 등 여러 중요한 장점을 제공한다. 내 프로그래밍 흐름에서는 이것이 매우 편리한데, 타입 추론의 안내를 받으며 structure (모듈 구현)를 작성하고, signature (모듈 인터페이스) 안에서 API를 명시하면서 표시 순서를 정하고 구현 공간을 어지럽히지 않는 명확한 문서를 작성할 수 있기 때문이다. 또한 캡슐화를 통해, 예를 들어 프로그램의 상태 기계를 표현하기 위해 intermediate types를 structure 안에서 자유롭게 정의하되, 밖으로 새지 않게 할 수 있다.
데이터 구조를 기술하는 강력한 도구: 타입을 추상화하고(구현을 숨기고) 이를 캡슐화와 결합하면, 불변성을 유지하는 데이터 구조를 기술할 수 있다. 그래서 각각의 데이터 구조마다 structure/signature 쌍을 두고, 추상화와 캡슐화를 통해 구현 세부를 숨기는 일이 흔하다.
재사용성과 공유: 값 언어 안에서 타입을 기술할 수 있듯이(대수적 타입에서 보았듯이), 모듈 언어 안에서도 타입을 기술할 수 있는데 이를 translucent signatures 라고 하며, structure와 결합하지 않고 signature의 타입을 정의할 수 있게 한다. 이 signature들은 구조적으로 타입이 붙으며, functors(모듈 언어에서의 함수)와 결합하면 모듈들 사이에 동작을 공유 할 수 있다.
고급 다형성 형태들: 모듈 언어에서 사용할 수 있는 Higher Kinded Polymorphism을 포함한다. 대체로 말해 "제네릭을 매개변수로 받는 제네릭" 을 기술할 수 있다. F#이나 Java 같은 언어에서 이 제한은 종종 그 부재를 우회하기 위해 무거운 인코딩을 사용하게 만든다.
ML 계열 언어에서 모듈 언어의 이론은 방대한 주제이며, 지금도 계속 진화 중이고, 단락 하나로 요약하기가 매우 어렵다. 하지만 Understanding and Evolving the ML Module System 라는 Derek Dreyer의 논문 서론은, 많은 예시와 함께 모듈의 목적과 사용을 훌륭하게 설명한다. 앞으로 몇 주 혹은 몇 달 안에, 내가 이미 시도했던 것보다 훨씬 더 자세히 모듈 언어에 대해 글을 써보기를 바란다. 매우 교육적일 수 있고, 내 생각에 이 주제는 엄청나게 흥미롭기 때문이다!
OCaml에서 객체 지향 프로그래밍을 짧게 언급하면서, 나는 OCaml이 언어 차원의 기능들을 통해 SOLID 한 코드를 작성하기 위한 전제들을 손쉽게 만족시킨다고 말했다. 마지막으로 강조하고 싶은 점은, 언어가 제공하는 기능들 을 통해 의존성 역전을 쉽게 달성할 수 있다는 것이다. 대체로 의존성 역전의 원리는 의존성 격자를 구현 이 아니라 추상화 로 기술하는 것이다. 이렇게 하면 이후에 의존성을 주입 할 수 있고 — 예를 들어 unit testing에서 문맥을 바꾸는 일이 아주 쉽게 가능해진다.
OCaml은 (적어도) 이러한 역전을 쉽게 만들어 주는 두 가지 도구를 제공하며, 각각은 서로 다른 맥락에서 유용하다. 의존성을 어떻게 역전할 수 있는지 보여주기 위해 매우 유명한 teletype 예시를 가져오자.
let program () =
let () = print_endline "Hello World" in
let () = print_endline "What is your name?" in
let name = read_line () in
print_endline ("Hello " ^ name)
겉보기에는 분명하지 않을 수 있지만, 이 프로그램은 구체적인 구현들 — 즉 표준 입력과 출력과의 상호작용 — 에 의존한다.
가장 직접적인 접근은 모듈을 사용하는 것이다. 일급 값으로 쓰거나, functors를 사용해 구성할 수 있다. signature와 structure의 이중성은 의존성 역전을 자명하게 만든다. 예를 들어 앞의 예시를 다시 보면, 일급 모듈을 사용해 추상적인 상호작용 집합에 의존하도록 만드는 것은 아주 쉽다. 먼저 가능한 상호작용의 추상 표현을 기술한다.
module type IO = sig
val print_endline : string -> unit
val read_line : unit -> string
end
이제 program 함수가 IO 타입의 모듈을 인자로 받도록 만들 수 있다(이를 handler 라 부르자). 그리고 예시에서는 Handler 라고 이름 붙인 모듈이 내보낸 함수들을 사용한다.
let program (module Handler: IO) =
let () = Handler.print_endline "Hello World" in
let () = Handler.print_endline "What is your name?" in
let name = Handler.read_line () in
Handler.print_endline ("Hello " ^ name)
예를 들어 unit testing의 맥락에서는, 호출된 모든 작업을 기록하고 read_line 호출을 mock 하여 반환값을 고정하는 구현을 제공할 수 있다. 이렇게 하면 비즈니스 로직을 검증하는 unit tests를 매우 쉽게 작성할 수 있다.
우리 함수에 구체적 구현을 인자로 넘기는 것은 곧 프로그램을 해석하는 것 과 같다.
OCaml 5 버전은 수많은 새로운 기능들과 함께 도착했다. 그러나 가장 큰 진전은 멀티코어 실행을 지원하기 위해 OCaml runtime 을 완전히 재설계한 것이다. 동시성 알고리즘을 기술하는 방법은 여러 가지가 있다 — 예를 들어 actors나 channels를 사용할 수 있다. OCaml은 프로그램의 제어 흐름 관리를 단순화하는 effects에 기대는 쪽을 선택했다. 실제로 OCaml은 사용자가 자신의 effect를 정의할 수 있게 하며, 이는 자연스럽게 user-defined effects라고 불린다. 이것들은 동시성 프로그램을 기술하는 강력한 도구일 뿐 아니라, 프로그램의 실행 흐름에 대해 handler 수준에서 제어를 유지하고 싶을 때 의존성을 주입하는 것도 쉽게 해준다.
참고: 내 예시에서는 방금 병합된 실험적 문법을 사용하고 있는데, 아마 언어의
5.3.0버전에서 사용 가능해질 것이다.
앞선 개선과 마찬가지로, 먼저 수행될 수 있는 연산들의 집합을 기술해야 한다. 이를 위해 effect 구문을 사용한다.
effect Print_endline : string -> unit
effect Read_line : unit -> string
그다음 effect를 발생시키는 방식으로, direct style로 프로그램을 작성할 수 있다.
let program () =
let () = Effect.perform (Print_endline "Hello World") in
let () = Effect.perform (Print_endline "What is your name?") in
let name = Effect.perform (Read_line ()) in
Effect.perform (Print_endline ("Hello " ^ name))
그 후에는 pattern matching과 유사한 구성을 사용해, 각 effect에 특정한 의미를 부여하면서 나중에 프로그램을 해석하는 것 이 가능해진다.
현재로서는 effect의 전파가 타입 시스템에 의해 추적되지 않는다 는 점을 주목해야 한다. 하지만 이것은 실험적 기능이며, YOCaml의 새 버전에서 광범위하게 사용되고 있다. 그리고 effect 전파를 추적하는 효율적인 타입 시스템 을 개발하기 위해 자원이 투입되고 있다는 사실도 알고 있다!
일반적으로, 프로그램의 흐름을 제어하는 데 관심이 없거나, 나중에 effect를 사후적으로 추가할 필요가 없을 때 나는 모듈을 사용한다. 반대로 YOCaml의 경우, 새로운 effect 시스템을 활용해 unit testing 전용 effect를 도입했고, 예를 들어 시간의 흐름을 mock 하는 것 이 가능해졌다.
다시 한 번 말하지만, 사용자 정의 effect 라는 이 새롭고 매우 흥미로운 기능에 대해 길게 늘어놓지 않기란 정말 어렵다. 여기서는 Arthur Wendling이 쓴 두 편의 글만 소개하며 마무리하겠다. 이 글들은 effects의 사용을 아주 교육적으로 설명해 주고, 함수형 프로그래밍에서 effect 추상화와 관련된 문헌들에 대한 종합적인 참고문헌 목록도 함께 제공한다.
덧붙이자면, 이런 역전/주입은 records 나 objects 를 사용해서도 할 수 있다. 하지만 OCaml에서의 내 경험상, 모듈을 쓰거나(혹은 프로그램의 제어 흐름을 조작하고 싶을 때 effect를 쓰는) 접근이 대체로 더 직관적이고 추론하기 쉽다.
OCaml은 버전이 바뀔 때마다 변화하는 끊임없이 진화하는 언어다. 의존성 역전 섹션에서 나는 멀티코어 runtime을 기술하기 위해 최근 effects가 언어에 포함되었다는 점을 짧게 언급했는데, 이는 수년간 이어져 온 OCaml의 진화를 보여준다. binding operators의 통합도 주목할 수 있는데, 이는 Functors, Applicative Functors, Monads의 삼박자를 더 편리하게 사용할 수 있게 해준다 — F#의 computation expressions와 비슷한 방식으로 말이다.
현재도 언어를 더 개선하기 위한 매우 흥미로운 프로젝트들이 많이 진행 중이다.
새로 추가된 문법, 연산과 effect의 분리, 그리고 물론 타입 시스템 안에서의 effect 전파에 관한 연구들을 포함하는, effects의 표현에 대한 심도 있는 작업.
Jane Street는 Rust에서 영감을 받은 비침습적 자원 관리 모델을 제안했으며, 여기에는 modalities 와 약간의 선형성 이 도입된다.
모듈 언어에 대한 진정한 기초 작업이 시작되었고, 이는 Modular Implicits의 구현을 더 매끄럽게 가능하게 할 것이다.
이 밖에도 위생적인 매크로 시스템의 개발, 단계적 메타프로그래밍 시스템의 점진적 통합, 그리고 optimization back-end의 구현도 주목할 만하다. 이 모든 것은 혁신 분야에서 OCaml이 매우 활발하다는 사실을 반영하며, 앞으로 몇 년간의 발전을 아주 의욕적이고 흥미롭게 만든다!
나는 OCaml이 훌륭한 언어 라고 확신하지만, 이 언어가 완벽하다고 주장하는 것은 아마 정직하지 못한 일 일 것이다 — 결국 완벽한 것은 없다. 내 생각에 언어로서의 OCaml에 그림자를 드리우는 몇 가지 지점을 들자면 다음과 같다.
ad-hoc polymorphism의 부재. 예를 들어 local module opening을 사용하는 식으로 우회할 수는 있지만, ad-hoc polymorphism 이 없다는 점(type classes — Haskell처럼, 또는 traits/implicit objects — Rust와 Scala처럼, 혹은 canonical structures — Coq처럼)이 때때로 어떤 상황들을 까다롭게 만든다. 나는 명시적 관계를 선호하는 편이지만, 세월이 지나며 이 부재가 문제를 일으키는 사례를 여러 번 봤다.
하지만 implicit modules의 도입이 단기 로드맵에 있지는 않더라도, “OCaml의 미래” 섹션에서 언급한 모듈 언어에 대한 최근 작업은 고무적이다.
모듈 언어와 값 언어 사이의 상호작용이 번거롭다. 모듈 언어는 자체 타입 시스템을 가진 다른 언어 다. 이것을 약점으로 볼지 여부는 논쟁의 여지가 있지만, 이 구분은 위협적으로 느껴질 수 있다. 이는 OCaml의 모듈 시스템이 모듈 이론의 선구자였고 더 최근의 혁신들(예: 1ML)보다 앞서 있었기 때문이다. 실제로는, 이해하기 복잡한 것 외에도, 언어의 몇몇 부분은 올바르게 명세하기 어렵다. 예를 들어 recursive modules가 그렇다.
함수형 프로그래밍에는 편안하지만 순수하지는 않은 언어. 나는 비순수성이 기능 이라고 생각하지만, 순수 함수형 언어(예: Haskell)에서 온 관용구를 가져오면 타입 추론과 관련된 어려움, 이를테면 value restriction 같은 문제가 생길 수 있다. OCaml이 이 제한을 완화하긴 했지만, 다형 함수 추론에 미치는 그 함의는 여전히 충분히 위협적으로 느껴질 수 있다 — 물론 매우 정당한 이유들 때문에.
문법. 개인적으로 나는 OCaml 문법을 정말 좋아하고, 문법이 대체로 큰 문제가 되어서는 안 된다고 믿지만, 몇몇 선택은 혼란스러울 수 있다. 예를 들어 타입 매개변수는 타입 이름 앞에 붙는다: a의 리스트는 'a list로 쓴다. 이런 선택들 다수는 구문적 모호성을 줄이기 위한 것이고, 금방 익숙해질 수 있다. 다만 다른 언어에서 왔다면, 이런 관례들 중 일부는 놀랍게 느껴질 수 있다.
이 약점들은 대체로 정당화될 수 있기 때문에 논쟁의 여지가 있다고 생각하지만, 그것들이 불편하게 느껴질 수 있다는 점은 충분히 이해한다. 하지만 이것들이 OCaml을 사용할 수 없게 만들 정도는 아니며, OCaml 입문에 큰 장벽이 되어서는 안 된다 고 생각한다! 개선 가능한 언어의 장점은 늘 개선의 여지를 제공하고, 다른 언어들에도 이익이 될 수 있는 작업을 촉진한다는 점이다. 그리고 아주 솔직히 말하자면, 이런 거친 면들 을 알고 있으면서도, 나는 OCaml을 쓰면서 그 거친 면들을 불평하기보다 오히려 다른 언어들에는 OCaml에 존재하는 기능들이 없다는 사실 에서 더 자주 좌절을 느꼈다. 이런 거친 면들에는 대개 해결책이 있고(가끔은 부분적으로만 만족스럽다는 점은 인정한다), 덕분에 차분하고 효율적으로 작업할 수 있다.
나는 아주 큰 줄기에서, 왜 OCaml을 배우는 것이 매우 적절한 선택인지에 대한 이유들 을 정리해 보았다. 이 언어는 어떤 아주 대중적인 프로그래밍 관용구들(종종 제대로 정의되지 않은 것들)을 근본적으로 이해할 수 있게 해준다. 더욱이 언어의 몇몇 측면은 산업적 목적에도 완벽히 부합하여, 좋은 실천들을 때로는 자명하게 표현하게 해준다! 이런 매력의 상당 부분은 다른 언어들에서도 실험해 볼 수 있지만, OCaml의 강하게 멀티 패러다임적인 성격은 이런 학습을 하나의 언어 안에 집중시킬 수 있게 해준다. 내가 아는 한, 부분적으로 대중적인 언어들의 정글 속에서 이만큼 많은 주제를 다루는 언어는 Scala 정도뿐인데, 내 관점에서는 다른 JVM 언어들과의 상호운용성 때문에 그 객체 모델이 훨씬 덜 흥미롭다.
이 글의 목표는 튜토리얼이 아니므로, 나는 modules와 effects 같은 몇몇 개념을 의도적으로 아주 얕게 다뤘다. objects, polymorphic variants, generalized algebraic types에 대해서는 거의 언급하지 않았다. 만약 이런 주제들이 흥미롭다면, Didier Rémy의 뛰어난 자료인 Using, Understanding, and Unraveling The OCaml Language와, 서론에서 소개했던 책들을 자세히 읽어보길 권한다. OCaml을 더 깊이 알고 싶은 사람에게는 그야말로 금광 같은 자료들이다.
결론적으로, OCaml은 프로그래밍을 배우고, 표준을 따르는 산업 수준의 프로그램을 만들고, 복잡한 데이터 구조와 범주론 기반의 추상화를 구현하기 위한, 언어 차원의 다양하고 풍부한 도구 집합을 제공한다. 예를 들어 함수형 코어, 명령형 특성, 풍부하고 표현력 있는 추론형 타입 시스템(대수적 타입 표현과 명확한 도메인 모델링을 가능하게 함), 추상화·재사용·컴파일 단위 정의를 위한 모듈 시스템, 객체 모델, 사후적으로 전파·해석될 수 있는 effect를 표현하는 능력, 그리고 다른 고급 기능들이 있다. 단지 고급 프로그래밍 개념을 파악하기 위해서라도, OCaml은 아주 훌륭한 후보 다 — 그래서 수많은 더 최근의 언어들이 OCaml로부터 분명한 영감을 받았고, Rust는 그 대표적인 예시다.
표현력 있는 언어를 갖는 것은 무언가를 만드는 데 아주 유익하다(이 표현이 의도적으로 순진하다는 점은 인정한다). 하지만 다양한 맥락에서, 직업적이든 개인적이든, 그것만으로는 충분하지 않다.
직업적 맥락에서는, 내가 속한 팀과 내가 생산성을 내고 싶다면 실제 문제를 해결하기도 전에 전체 도구 스택을 먼저 만들어야 하는 상황은 아마 그리 적절하지 않을 것이다.
개인적 맥락에서는, 기술 스택을 직접 구축하는 것이 매우 교육적 이라고 주장할 수도 있겠지만, 그렇게 되면 실제로 개발하고 싶은 역량의 집합이 달라진다. 예를 들어 웹 언어로서의 OCaml을 시작해 보기 위해 작은 웹 애플리케이션 하나를 만들고 싶은데 전체 HTTP 스택을 전부 스스로 구현해야 한다면, OCaml은 아마 올바른 선택이 아닐 것이다. 다행히도 OCaml에는 웹 애플리케이션을 만들기 위한 풍부한 도구 생태계가 있다!
그래서 언어가 제공하는 기능들만으로는 프로젝트를 구축하고 유지하는 데 있어 그 언어의 실용성을 설명하기에 충분하지 않다. 생태계 역시 아주 중요한 요소다. 그리고 이것이 바로, 상대적으로 표현력은 덜하지만(그래도 계속 개선되고 있는) Java나 C# 같은 언어를 가진 .NET과 JVM이 여전히 그렇게 인기 있는 이유이기도 하다. 생태계의 적절성을 평가하려면, 몇 가지 기준을 고려하는 것이 중요하다고 생각한다.
프로젝트에 대해 runtime (또는 컴파일 타깃)이 적절한가. 아주 작고 이국적인 하드웨어 에 임베딩하는 용도라면, 아마 나는 OCaml을 권하지 않을 것이다 — 물론 저수준 프로그래밍에 대해 아는 것이 전혀 없으니(내 분야가 전혀 아니기 때문에) 틀릴 수도 있다.
그 플랫폼. 전체 toolchain 이 완전하고 인체공학적인가? 내 관점에서 여기에는 패키지 관리자, build system, 좋은 editor support (가능한 한 도구에 종속되지 않는), 견고한 문서 생성기, 그리고 formatter 같은 각종 추가 도구들이 포함된다.
사용 가능한 라이브러리들 이 해당 프로젝트에 얼마나 적절한가(그리고 이것들이 얼마나 잘 유지관리되고 발견 가능성이 높은가 — 이는 보통 패키지 관리자의 존재를 뜻한다). 예를 들어 암호화 기본 요소가 전혀 없다면, blockchain 을 만들기 위해 이 기술을 고르지는 않을 것이다. 혼자서 또는 직업적 맥락에서 독립적으로 해결하기가 매우 어려운 문제들의 부류가 분명히 존재한다.
이 섹션에서는 OCaml 생태계가 언어 자체에 걸맞은지를 보기 위해 이런 다양한 점들을 훑어보려 한다. 다만 나는 다소 편향되어 있다 는 점을 밝혀두고 싶다. 왜냐하면 나는 생태계가 지금보다 훨씬 빈약했던 2012년부터 이미 OCaml의 적절성을 확신하고 있었기 때문이다. 그 시절 나는 빈틈을 메우며 프로젝트를 만들려 했고, 그것이 아마 생존자 편향을 만들었을 것이다. 오늘날에는 산업 사용자들 덕분이기도 해서 OCaml 생태계가 훨씬 풍부하고 넓어졌고, 그래서 옹호하기가 훨씬 쉬워졌다. 다만 여전히 공백이 남아 있는 부분에서는 오래된 사용자 의 나쁜 신앙이 되살아날 때도 있다.
OCaml은 처음부터 두 가지 컴파일 타깃을 갖고 있었다.
네이티브 컴파일: 특정 아키텍처용으로 컴파일된 매우 효율적인 실행 파일을 생성하며, 많은 아키텍처를 지원한다. 더불어 Windows는 역사적으로 상당히 소홀히 다뤄졌지만, 특별한 노력이 이를 지원하기 위해 이루어졌다(DkMl 프로젝트라는 독립적 시도도 참고하라).
바이트코드 (가상 머신용) 컴파일: 이식 가능한 실행 파일을 생성한다.
가상 머신의 존재 덕분에 오래된 Js_of_OCaml이 발전할 수 있었고, 이는 OCaml 바이트코드를 JavaScript로 변환할 수 있게 해준다. 그 결과, OCaml은 브라우저 안의 애플리케이션뿐 아니라 Node runtime에서도 충분히 실용적이 되었고, 이 웹사이트에서도 광범위하게 사용되고 있다. 유사한 접근을 사용하여 WebAssembly 지원도 아주 최근 Wasm_of_OCaml 프로젝트를 통해 가능해졌다. 가비지 컬렉터를 가진 언어에 대해 WASM 컴파일을 지원하는 것은 중대한 도전 이었지만, 최근 WASM 과 garbage collectors 사이의 상호작용이 명세되면서 OCaml은 이제 아주 괜찮은 WebAssembly 컴파일을 갖게 되었다 (그리고 Ocsigen 같은 많은 야심찬 웹 프로젝트들이 WASM 을 네이티브로 지원하기 시작하고 있다).
게다가 Melange 프로젝트(역사적으로 BuckleScript)는 JavaScript를 만들기 위한 대안으로, OCaml AST 를 JavaScript AST 로 대응시키는 방식의 transpile 을 제공한다. Js_of_OCaml과 Melange를 비교하자면, JavaScript를 생성하는 기반 방식의 차이(바이트코드로 컴파일한 뒤 그 bytecode 를 JavaScript로 바꾸는 것과, OCaml에서 JavaScript로의 구문 변환) 외에도, 나는 Js_of_OCaml 이 OCaml 생태계와 더 잘 통합되므로 아마도 브라우저에서 접근 가능한 프로젝트를 만들고 싶은 OCaml 개발자들 을 겨냥한다고 말하고 싶다 — 실제로 기존 JavaScript 생태계와의 상호작용은 더 번거로울 수 있다. 반면 Melange 는 JavaScript 생태계(npm 등)에 더 잘 맞기 때문에, 자기 JS 프로젝트(혹은 기존 코드베이스)에 더 많은 안전성을 가져오려는 JavaScript 개발자들 을 더 겨냥한다고 볼 수 있다.
오늘날 Idris나 Nim 같은 멀티 백엔드 언어를 만나는 일은 흔하다. 하지만 그 당시 내가 매우 인상 깊게 느꼈던 것은, OCaml이 내가 사용하기 시작했을 때부터 JavaScript로도 컴파일할 수 있었다는 점이다. 그때 내가 알던 여러 컴파일 타깃을 제공하는 유일한 언어는 Haxe였는데, 그 타깃들은 너무나도 달랐다(참고로 Haxe는 OCaml로 작성되었다).
실제로 2024년에는 JavaScript를 생성하는 것이 표준처럼 되었지만, Js_of_OCaml의 첫 흔적은 2006년으로 거슬러 올라간다. 이 점에서 OCaml은 이 분야의 선구자였다!
OCaml의 다양한 실행 및 컴파일 맥락들이 이루는 격자 안에서, 대다수 맥락에서 잘 동작하는 라이브러리를 만드는 것은 어려운 과제다. 다행히 MirageOS 프로젝트 — 가상화를 통해 오직 하나의 애플리케이션만 실행하기 위한 전용 운영체제 (unikernel)를 구축하기 위해 설계된 라이브러리 집합 — 는 멀티컨텍스트 라이브러리를 생산하기 위한 진정한 규율을 도입했다.
가까운 미래에 나는 Mirage에 대해 더 많은 글을 쓰는 데 시간을 들이고 싶다. 우리도 이 흥미로운 프로젝트를 YOCaml 같은 프로젝트에 통합하려고 하고 있기 때문이다. 또한 지능적으로 구획된 라이브러리를 배포하는 건전한 접근을 제공할 뿐 아니라, Mirage는 OCaml 프로젝트를 구축하기 위한 튼튼한 라이브러리 기반도 제공한다. 이에 대해서는 라이브러리 섹션에서 더 자세히 이야기하겠다.
OCaml 플랫폼은 명시적인 생명주기(active, incubating, maintained, deprecated) 안에서 유지되는 도구 집합으로, OCaml 코드 생산을 위한 일관된 도구 체계로 컴파일러를 보조하도록 설계되어 있다. 다양한 용도의 많은 도구들을 포함하고 있지만, 이 섹션에서는 그중 몇 가지 측면에만 집중하고, 자세한 정보는 플랫폼 페이지와 로드맵을 직접 살펴보길 권한다. 여기서는 아주 큰 줄기에서, 다음의 네 가지를 살펴보겠다.
OCaml을 어느 정도 사용해 본 사람에게는, 아마 이 부분이 이 글에서 가장 흥미로운 부분일 것이다. 내 생각에는 이 부분이야말로 가장 많은 진전을 이룬 영역이기 때문이다. 그리고 로드맵은, 내 보기엔, 매우 유망하다!
언어 전용 패키지 관리자 는 오늘날 언어 채택 장벽을 낮추는 데 매우 중요한 요소(사실상 필수 요소)가 되었지만, OCaml이 설계되던 당시에는 이런 것이 드물었다. 실제로 TeX 패키지를 배포하기 위한 CTAN, CTAN에서 영감을 받아 Perl 패키지를 배포하기 위한 CPAN, 그리고 PHP를 위한 PEAR 정도를 제외하면, 프로그래밍 언어가 패키지 관리자를 언어의 당연한 일부로 받아들이는 것은 Gems에 이르러서야 본격화되었다.
OPAM은 OCaml Package Manager의 약자로, 2012년에 나온 제안이다(공식 사이트의 About 페이지에는 짧은 타임라인도 있다). 패키지 설치뿐 아니라 OPAM은 다양한 OCaml 버전을 설치하고, switches라고 불리는 잠재적으로 샌드박스화된 환경 을 만들 수 있게 해준다. GitHub에 호스팅된 공개 리소스 저장소를 사용할 수도 있고, 자신만의 패키지 인덱스를 만드는 것도 완전히 가능하다.
이미 여러 패키지를 OPAM에 배포해 본 입장에서, 패키지 추가 검증용 CI는 믿을 수 없을 정도로 효율적이고 친절하다고 인정해야 한다(모든 에러에 대해 로컬에서 문제를 재현할 수 있는 Dockerfile이 제공된다). 그리고 패키지 추가/수정 사항을 검토하고 관리하는 팀은 엄청나게 빠르고 친절하다.
현대적 기준으로 보면 OPAM에 대해 몇 가지 비판을 제기할 수는 있다. 예를 들면:
dune 과의 상호작용은 더 매끄러울 수 있는데, 이 부분도 현재 작업이 진행 중이다)그럼에도 OPAM이 없던 시대에서 온 사람으로서, 나는 이런 사소한 함정들과 함께 살아가는 법을 배웠고, 일상적으로는 이 도구에 대해 불평할 이유가 거의 없다. 실제 사용에서 나를 실망시킨 적이 사실상 없기 때문이다. 물론 사용 중 문제가 있었다면, 커뮤니케이션 공간들 가운데 하나에서 논의해 보길 권한다. 개발팀이 여러분의 피드백을 고려하고 안내해 줄 수 있을 것이다.
대안 패키지 관리자로는 esy도 있다. 이는 Nix에서 영감을 받아 재사용 가능한 store 를 구축하는데, Nix를 OCaml과 함께 사용할 수 있는 것과 같은 방식이다. 다만 나는 다소 전통적인 편이라 이런 관행들에는 그다지 익숙하지 않고, OPAM 기반 workflow 에 만족하고 있어서, 안타깝게도 esy 를 진지하게 실험해 볼 시간을 한 번도 내지 못했다.
패키지 관리와 마찬가지로, 역사적으로 OCaml에는 여러 개의 build-systems 가 있었다: 오래된 ocamlbuild, oasis, ocp-build, Jenga, 그리고 Make를 둘러싼 여러 변형들. 그러나 2018년 이후로 커뮤니티는 Janestreet에서 처음 개발된 build-system 인 Dune을 강하게 채택했다.
많은 면에서 Dune은 위협적으로 느껴질 수 있다. 실제로 그 문서는 매우 밀도가 높다 — 다만 최근 몇 달 동안 구조 측면에서 크게 개선되었다. 그리고 많은 도구들이 YAML, TOML, 혹은 JSON 같은 규칙 기술 언어를 선택하는 반면, Dune은 S-expressions를 선택했다. 또한 Dune이 기본적으로 모든 경고를 치명적인 것으로 간주한다는 점은 아쉽다.
그 선택들(예컨대 S-expressions)을 설명하기 전에, Dune이 표준이 되게 만든 지점들을 강조하는 것이 매우 중요하다.
make처럼 임의의 작업을 실행할 수 있다내가 편향되었을 수도 있지만, 내 생각에 Dune은 내가 사용해 본 build-systems 가운데 가장 범용적이고 쾌적한 것 중 하나다 — 처음에는 위협적으로 보일 수 있고 어떤 선택들은 정당화하기 어렵게 느껴질 수 있지만 말이다.
언뜻 보면, 바이너리·라이브러리·프로젝트를 기술하기 위해 Lisp 같은 문법을 사용하는 것은 놀라울 수 있다. 하지만 이 결정에는 여러 장점이 있다.
그러므로 내 관점에서 S-expressions 의 선택은 적절하다. 너무 장황하지 않으면서 복잡하고 읽기 쉬운 프로그램을 기술할 수 있고, 컴파일을 크게 느리게 하지 않으며, 아주 복잡한 빌드 규칙도 매우 간결하게 기술하게 해준다. 그리고 아주 솔직히 말하면, 금방 익숙해진다!
Dune은 쾌적한 build-system 일 뿐만 아니라, 범주론에서 영감을 받은 새로운 구성을 부각함으로써 연구의 최신 상태에도 기여했다. 실제로 2018년 Andrey Mokhov, Neil Mitchell, Simon Peyton Jones는 뛰어난 논문 "Build Systems à la Carte"에서, 다양한 build-systems 를 모듈식으로 재구현할 수 있는 추상화 집합을 제안했다. 하지만 정적 의존성 분석 과 관련된 이유로, 이 모델들은 Dune과는 호환되지 않았다. 여러 조사와 실험 끝에, Applicative와 유사한 새로운 구성인 Selective Applicative Functor가 제안되었고, 이는 Dune의 요구사항을 포착한다. 이 정보는 사소해 보일 수 있지만, 내 생각에는 연구와 산업의 교차점 에 있다는 것의 가치(와 중요성)를 다시 한 번 보여준다.
비록 커뮤니티에서 널리 채택되었지만, OCaml은 다른 시스템들도 제공한다(때로는 내부적으로 Dune을 활용하면서). 예를 들어 Bazel을 위한 OCaml 규칙을 제공하는 Obazl, Nix로 프로젝트를 빌드할 수 있게 해주는 Onix, Bazel과 경쟁하는 야심찬 범용 프로젝트인 Buck2, 그리고 패키지 관리와 프로젝트 빌드를 통합하여 Cargo와 비슷한 경험을 제공하는 Drom이 있다.
앞선 섹션들에서 우리는 OCaml이 산업화에 필요한 영역들에서 얼마나 크게 진전했는지 보았다. 반면 에디터 지원 측면에서는, OCaml은 Merlin 프로젝트를 통해 10년 넘게 Vim과 Emacs에 훌륭한 지원을 제공해 왔다. Merlin은 에디터 서비스를 제공하는데, 이를 통해 자동 완성, 진단, 코드 탐색, 값 분해, 값 구성, typed holes 의 관리(및 탐색), 극성 기반 검색, 값 타입에 대한 정밀한 정보(자세함 조절 포함), jump-to-definition 등을 가능하게 한다.
내 생각에 Merlin을 통한 IDE 지원은 아주 오랫동안 OCaml에서 훌륭했다. 여기에 에디터 안에서 특정 동작 이후 커서 위치를 계산해 주는 ocp-indent, 그리고 OCaml 파일을 실시간으로(설정 가능하게) 포맷해 주는 OCamlformat이 결합되면, Emacs나 Vim에서 코드를 쓰는 일은 그야말로 즐거움이다!
2015년 Visual Studio Code가 등장하면서 Language Server Protocol도 함께 등장했다. 이것은 에디터가 서버를 통해 언어와 상호작용하는 방식을 표준화한 프로토콜이다. OCaml은 아주 훌륭한 LSP 서버를 갖고 있으며, 이 서버는 특히 Merlin 같은 OCaml 생태계의 잘 확립된 라이브러리들에 기반한다. 이제 LSP가 에디터 세계에서 상대적으로 표준 이 되었기 때문에(Vim, Emacs, 그리고 내가 아는 거의 모든 자유 소프트웨어 에디터들이 LSP 서버와 상호작용할 수 있다), 계획은 Merlin 서버를 점차 폐기하고 LSP로 완전히 이동하는 것이다. 그렇게 되면 Merlin은 LSP가 사용하는 도구를 제공하는 저수준 라이브러리가 된다. 이것은 Tarides(내가 속한)의 Editor 팀이 작업 중인 프로젝트 가운데 하나다: ocaml-lsp를 Merlin의 기존 서버와 기능 면에서 호환되게 만들어, 대체 클라이언트(Emacs와 Vim)의 유지보수 부담을 줄이고, 오직 OCaml 전용 요청과 액션만 걱정하도록 하는 것이다(당연히 그것들은 프로토콜의 일부가 아니기 때문이다).
현재 Visual Studio Code용 OCaml 플랫폼과 OCaml-eglot이 OCaml용 LSP 확장을 구현하는 두 가지 정식 구현체이며, 각각 VSCode와 Emacs를 대상으로 한다. 우리는 현재 NeoVim 플러그인 구현도 고려하고 있다.
Dune과 비슷하게, 내 생각에 이 도구 환경은 훌륭하고 로드맵도 고무적이다! 다만 이건 내 일 이기도 하므로, 아마 나는 편향되어 있을 것이다.
OCaml은 오랜 문서 생성기 OCamldoc과 함께 배포되지만, 커뮤니티는 더 이상 그것을 권장하지 않는다. 대신 추천되는 도구는 컴파일러 바깥에서 존재하며 여러 흥미로운 기능을 제공하는 새로운 도구 Odoc이다.
Odoc이 생성한 문서의 look'n feel 이 OCamldoc이 만든 것보다, 내 생각에는, 훨씬 우수 하지만, 도구가 진정으로 완벽 해지려면 UI 쪽에 약간의 작업이 더 필요하다고 생각한다!
나는 Elixir 언어의 문서 시스템 HexDoc에 대해 분명한 애정을 갖고 있다(design 과 기능 측면에서), 개인적으로는 OCaml도 그 예시에 가까워졌으면 한다. 하지만 Odoc이 생성하는 문서가 다른 많은 언어들의 문서보다 낫다는 점은 인정해야 한다. 게다가 이 언어의 매우 모듈적인 특성 때문에, 상호 참조 를 효과적으로 지원하는 좋은 문서 생성기를 갖는 것 자체가 대단한 성취다!
우리는 언어가 멋지고, 도구 체계도 여전히 진화 중이지만 효과적이고 사용하기 좋다는 점을 보았다. 그렇다면 그 낮은 인지도는 라이브러리 집합이 너무 제한적이기 때문일까? 아주 솔직히 말해, 나는 모르겠다. 다만 내가 알고 있는 것은, 내가 직업적으로든 개인적으로든 OCaml 프로젝트를 작성해야 했을 때, 패키지 목록에서 필요한 것을 대체로 모두 찾을 수 있었다는 점이다. OCaml이 많은 전형적인 프로젝트들에 충분히 성숙한 이유는 몇 가지로 요약할 수 있다고 생각한다.
나의 경우, 나는 때때로 바퀴를 다시 발명하는 즐거움 을 위해 라이브러리를 다시 만들기도 했고, 때로는 다른 인터페이스를 제공하기 위해 그렇게 하기도 했다. 또한 OCaml은 무엇보다도 C와 상호운용할 수 있기 때문에, 매우 많은 라이브러리와 도구들에 대한 binding 을 만들 수 있다. 하지만 만약 여러분이 객관적으로 빠져 있다고 느끼는 라이브러리가 있다면, 커뮤니티에 참여해 보길 권한다.
내 OCaml 사용은 주로 세 가지 영역에 집중되어 왔음을 밝혀두는 것이 중요하다.
이 모든 영역에는 여전히 좋은 테스트 도구가 필요하며, OCaml은 견고한 테스트 스위트를 구현하기 위한 여러 상호보완적 라이브러리를 제공한다. 실제로 OCaml 생태계 안에서는 doctests, 전통적인 unit tests, property-based tests, fuzzing, 그리고 출력 관찰 테스트, inline tests(이것은 private 컴포넌트들을 포함한 테스트를 가능하게 한다), cram tests를 작성할 도구들을 찾을 수 있다.
나는 여전히 제공되는 패키지들 사이에서 필요한 것을 모두 찾고 있고, 해가 갈수록 패키지 수와 대안들이 늘어나는 것을 보며 여전히 깊은 인상을 받는다. 물론 몇몇 공백은 있지만, 그것들이 내가 OCaml을 선택한 이유를 무효화하지는 못했다.
OCaml에 대한 반복적인 비판 중 하나는 표준 라이브러리가 소박하다 는 점이다. 역사적으로 그것은 언어 그 자체를 구현하기 위해 설계되었기 때문에, 최종 사용자에게 유용한 몇몇 기능들이 포함되지 않았다. 이런 상황은 대체 표준 라이브러리들의 등장을 낳았고, 그중 가장 유명한 것들은 다음과 같다.
f 로 붙임) 강한 규약을 강제한다.open Containers를 해도 표준 라이브러리로 작성된 코드를 깨뜨리지 않는다).이런 대체 표준 라이브러리들 외에도, 운영체제와 상호작용하기 위한 도구를 제공하는 Bos처럼 일반적인 문제를 다루는 특화 라이브러리들이 있고, 범주론의 추상화를 실현 하게 해주는 Preface도 있다 — 대놓고 하는 홍보다.
표준 라이브러리에 대한 유지관리자들의 입장은 세월에 따라 바뀌었고, 이제는 그것을 확장하는 것도 고려할 수 있게 되었다. 하지만 표준 라이브러리에 새 모듈을 추가하는 일은 종종 논쟁의 대상이며, 실제 추가까지 오랜 시간이 걸릴 수도 있다. 개인적으로는 표준 라이브러리가 계속 언어 개발만을 위한 역할 을 유지하고, 대신 OCaml 커뮤니티의 이름으로 별도 라이브러리가 배포되었으면 더 좋았을 것이다. 이런 분리는 언어와 표준 라이브러리의 릴리스를 비동기화할 수 있게 하고, 라이브러리와 언어의 호환성을 단순화할 가능성도 높인다.
안타깝게도 플랫폼의 모든 도구들, 혹은 OCaml을 개인 프로젝트와 산업 프로젝트 양쪽에서 즐겁게 사용하게 만들어 주는 핵심 구성 요소들을 모두 다룰 기회는 없다(예를 들어 존재하는 여러 디버거들 같은 것들). 하지만 OCaml을 사용하는 데 튼튼한 기반을 이루는 몇 가지 도구들에 대해 개관할 수는 있었기를 바란다.
내가 이 언어를 사용하는 동안, 때로는 직접 라이브러리를 만들어야 했던 적도 있었다. 하지만 나는 그 연습을 후회하지 않는다. 안타깝지만, 필요한 라이브러리의 100%가 준비되어 있지 않다는 이유만으로 어떤 언어를 결코 쓰지 않겠다고 결정한다면, 그것은 어딘가 수준을 낮추는 일 처럼 느껴진다. 결국 우리를 Java나 C#처럼 부유한 회사들 에 의해 뒷받침되는 언어들 뒤에 가둬 버리게 되고, 그건 조금 슬픈 일 이다.
비록 내가 많은 프로그래밍 언어를 사용해 보았지만, 내가 강한 커뮤니티 상호작용을 경험한 유일한 언어는 아마 OCaml인 것 같다. 그래서 다른 커뮤니티들이 어떻게 돌아가는지는 충분히 알지 못하고, 그런 의미에서 내 피드백은 다소 부적절할 수 있다. 하지만 내 경험으로는, OCaml 커뮤니티는 매우 생산적일 뿐 아니라 다음과 같은 특성이 있다.
매우 접근하기 쉽다: 다른 많은 언어들처럼, OCaml도 강한 온라인 존재감을 갖고 있다. 이 플랫폼들에서는 언어와 생태계에 대한 경험이 풍부한 기여자들을 만날 수 있고, 전문가의 조언(혹은 때로는 덜 기술적인 조언)도 받을 수 있다. 특별히 Gabriel Scherer와 Florian Angeletti를 언급하고 싶다. 그들의 답변은 늘 사려 깊고 흥미롭다.
매우 친절하다: 나는 자주 도움을 요청해야 하는 편인데, 개인적으로든 공개적으로든 늘 명확하고 정확한 답을 받았다.
매우 뛰어나다: OCaml은 탁월한 연구자들 의 작업 결과물이고, 그들과 상호작용할 기회를 갖는다는 것은 정말 놀라운 일이다(그리고 약간 위축될 수도 있다). 언어 설계의 주요 발견들 뒤에 있는 사람들에게 직접 질문할 수 있다는 것은 엄청난 기회다.
커뮤니티 측면의 결론으로, 비록 다른 커뮤니티들이 어떻게 상호작용하는지 완전히 알지는 못하지만, OCaml 개발자 커뮤니티의 일원이라는 것은 내게 즐거운 일이다. 이곳은 공유와 배움에 적합한, 환영받는 공간이다.
드디어 이 지나치게 긴 글의 가장 재미있는 부분에 도착했다. 이제 OCaml에 대한 몇 가지 끈질긴 신화들을 반박 할 수 있다. 물론 완전한 객관성을 보장할 수는 없지만, 내 의도는 선하다는 것만은 알아주면 좋겠다. 인터넷에서는 OCaml에 대한 이런저런 비판이나 언급을 자주 보게 되는데, 나는 거기에 응답하는 일이 종종 피곤하게 느껴진다. 하지만 이 언어에 대한 내 열정을 공유하려는 글보다, 이런 비판들 몇 가지를 차분히 다루고 답해 보기에 더 좋은 방식이 또 있을까?
몇 가지를 골라 보았지만, 앞으로는 HeyPlzLookAtMe (fr) 구성원들처럼, 내가 불공정하다고 느끼는 글들에 대해 조금 더 긴 글을 쓸 가능성도 있다.
F#은 역사적으로 OCaml에서 강하게 영감을 받은 프로그래밍 언어로, .NET 플랫폼 위에서 실행된다(그리고 사실상 C#과 매우 잘 통합된다). 나는 이 언어를 — DernierCri와 D-Edge에서 직업적으로 사용했다 — 매우 쾌적하다고 생각한다. 역사적으로 .NET은 Windows 환경에만 국한되어 있었기 때문에, OCaml은 비교에서 큰 손해를 보지 않았다. 하지만 크로스플랫폼 구현인 .NET Core가 등장한 이후, 나는 인터넷에서 다음과 같은 말을 점점 더 자주 본다.
"같은 언어인 F#이 있고, 전체 .NET 생태계도 있고, 더 많은 기능도 있고, 더 쓰기 좋은 문법도 있는데 왜 굳이 OCaml을 계속 쓰나?"
우선, .NET(Core) 생태계를 갖는 것은 확실히 큰 장점이라고 생각한다. 문법에 대해서는 조금 더 유보적이다. 실제로 들여쓰기 기반 문법은 때때로 코드를 이동시키는 일을 더 번거롭게 만들고, OCaml 문법에 대한 비판이 있더라도, 나는 그것이 나를 실망시킨 적은 없다고 인정해야 한다. 마지막 지점은 조금 더 교묘하다. 실제로 F#에는 OCaml에 없는 기능들이 도입되었다. 예를 들면:
이런 발전들은 언어에 점진적으로 들어왔다. 그렇다고 해서 OCaml이 진화하지 않았다고 생각하는 것은 순진한 일일 것이다. 실제로 두 언어가 역사적으로 매우 비슷해 보였다고 해도, F# 제안 초기부터 이미 어떤 기능들은 빠져 있었다.
모듈 언어 의 부재. 실제로 F#에도 module 키워드는 있지만, 그것은 정적 클래스를 기술하는 데만 사용되고(namespace와도 다소 어색하게 결합된다).
근본적으로 다른 객체 모델 (물론 C#과의 상호운용성을 위해서다).
이 두 가지 이유만으로도 OCaml과 F#은 사촌 언어이긴 하지만 매우 다른 언어라고 볼 수 있고, 내 생각에는 어느 한쪽을 더 선호할 충분한 이유가 된다. 내 경우에는 F#보다 OCaml을 선호하기 때문에, 이 섹션의 도입 문장은 무의미해진다. 하지만 F#처럼 OCaml도 진화했고, 이 두 가지 근본적 차이 외에도 F#에는 없지만 OCaml에는 있는 기능들이 많다.
opens: OCaml에서는 스코프 안에서 로컬하게 모듈을 열 수 있지만, F#에서는 top-level 에서만 모듈을 열 수 있어서 어떤 경우에는 꽤 답답할 수 있다.결론적으로, F#은 정말 좋은 언어이고 그것을 사용하면 많은 장점이 있지만(특히 .NET 플랫폼), 그냥 더 나은 버전의 OCaml은 아니다. 두 언어는 매우 다르고, 내 관점에서는 OCaml이 더 정교한 타입 시스템을 갖고 있어서 내가 F#보다 그것을 선호하게 된다. 내 생각에 F#이 그저 더 예쁜 OCaml이라고 말하는 것은, Kotlin이 Scala의 가벼운 문법 버전에 불과하다고 말하는 것만큼이나 타당하다.
표준 라이브러리에는 정수용 산술 연산자가 다음과 같이 들어 있다.
val ( + ) : int -> int -> int
val ( - ) : int -> int -> int
val ( * ) : int -> int -> int
그리고 부동소수점용 산술 연산자도 있다.
val ( +. ) : float -> float -> float
val ( -. ) : float -> float -> float
val ( *. ) : float -> float -> float
언뜻 보면 이것은 혼란스럽게 느껴질 수 있다. 하지만 완전히 말이 되는 설계다. 만약 우리가 제네릭 연산자를 갖고 싶다면, 예를 들어 Haskell처럼 ad-hoc polymorphism이 필요할 것이다. Haskell에서는 산술 연산자들이 Num 타입 클래스 안에 존재한다.
class Num a where
-- more code
(+), (-), (*) :: a -> a -> a
-- more code
연산자에 대한 제약, 예컨대 op :: Num a => a -> a -> a 같은 것을 기술하기 위해 클래스, 트레이트, implicit 같은 형태의 ad-hoc polymorphism이 없다면, 우리는 무엇을 할 수 있을까? 온라인에서 자주 보던 제안 하나는 = 연산자와 같은 요령 을 쓰는 것이었다. 그 타입은 val (=) : 'a -> 'a -> bool다. 하지만 이건 작동하지 않는다. 왜냐하면 모든 것은 비교 가능할지도 모른다 고 기대할 수는 있어도(최악의 경우 false 를 반환하면 되니까), 덧셈 같은 것을 어떻게 일반화할 수 있겠는가?
산술 연산자 지원은 까다로운 문제이며, 사실 type classes의 원래 동기이기도 하다(F#에서 statically resolved type parameters가 존재하는 이유이기도 하다). 내 관점에서는, modular implicits를 기다리는 동안, 정수와 부동소수점에 대해 연산자를 복제하는 것은 합리적인 접근처럼 보인다. 그리고 만약 어떤 이상한 이유로 float를 쓸 때 연산자 뒤에 점을 붙이는 일이 두드러기를 일으킨다면, 예를 들어 다음 모듈을 제공해서 local opens로 이를 피할 수 있다.
module Arithmetic (P : sig
type t
val add : t -> t -> t
val sub : t -> t -> t
val mul : t -> t -> t
val div : t -> t -> t
end) =
struct
let ( + ), ( - ), ( * ), ( / ) = P.(add, sub, mul, div)
end
이렇게 하면 이미 add, sub, mul, div 함수를 제공하는 Int 와 Float 모듈을 확장하여 산술 연산자를 부여할 수 있다.
module Int = struct
include Int
include Arithmetic (Int)
end
대체로 말하면, 우리는 Int 모듈을 만들고 기존 Int 모듈을 포함하여 새 Int 모듈이 원래 Int 모듈의 전체 API를 유지하게 한 뒤, 산술 연산자를 정의하고(그리고 포함한다). 이제 같은 과정을 Float 에도 반복할 수 있다.
module Float = struct
include Float
include Arithmetic (Float)
end
그러면 이제 연산자에 점을 붙이지 않도록 local open 을 사용할 수 있다.
let x = Int.(1 + 2 + 3 + (4 * 6 / 7))
let y = Float.(1.3 + 2.5 + 3.1 + (4.6 * 6.8 / 7.9))
내 관점에서는, 이런 점이 그것에 익숙하지 않은 언어에서 온 사람들에게 혼란을 줄 수는 있어도, 사소한 문제다. 연산자 오버로딩이 없다는 사실은 어떤 언어에 기회를 주지 않을 이유로서는 다소 약한 논거처럼 보인다 — 물론 이는 나의 겸손한 의견일 뿐이다.
ml 과 mli 분리에 대하여또 하나 많은 논의를 불러오는 지점(심지어 최근에도)은 ml 파일과 mli 파일의 분리 다. 개인적으로 나는 이것이 아주 좋다고 생각한다. 약간의 반복을 도입할 수는 있지만, mli 파일에서 모듈 캡슐화를 통해 API에 집중하면서 동시에 문서도 추가할 수 있기 때문이다. 공개할 함수들의 순서를 내가 원하는 대로 정리할 수 있고, 공유하는 타입도 자연스럽게 최대한 추상화할 수 있다. 더불어 구현을 볼 때 ml 코드는 문서로 어지럽혀져 있는 경우가 드물어서, 모듈의 각 요소를 탐색하기도 쉽다. 게다가 이것은 분리 컴파일을 가능하게 하고, 개발 중 다른 모듈의 구현만 바뀌었을 경우 그것에 의존하는 모듈들을 다시 컴파일하지 않게 해준다(이것이 Dune의 dev 프로파일 기본 동작이다).
하지만 취향은 다양하고, 복잡한 타입이나 module type을 노출할 때 이런 반복이 성가실 수 있다. 다행히 2020년에 Craig Ferguson이 소개한 요령 이 이런 반복을 줄이는 데 도움을 준다: The _intf_ trick.
추가로, open 과 include primitive에 임의의 module expressions를 넘길 수 있다는 점을 활용한 작은 요령들도 있어서, 때로는 mli 없이도 충분히 작업할 수 있다. 나는 이미 OCaml, modules and import schemes라는 글에서 이 점을 언급한 바 있다.
mli 없이 캡슐화하기인터페이스 없이도 코드 일부를 내보내지 않으려면, 그냥 open struct (* private code *) end 를 사용하면 된다. 예를 들어:
open struct
(* Private API *)
let f x = x
let g = _some_private_stuff
end
(* Public API *)
let a = f 10
let b = g + 11
ml 에서 인터페이스 표현하기또 다른 유사한 기법은 include (struct ... end : sig (* public API *) end) 를 사용하여 structure와 interface를 같은 파일 안에서 기술하는 것이다. 예를 들어:
include (struct
type t = int
let f x = x
let g = _some_private_stuff
end : sig
type t
val f : int -> t
end)
이렇게 하면 signature와 structure가 같은 파일 안에 있으면서도, 캡슐화를 정밀하게 제어할 수 있다. 또 다른 방법은 signature를 전용 module type 안에 넣는 것이다.
module type S = sig
type t
val f : int -> t
end
include (struct
type t = int
let f x = x
let g = _some_private_stuff
end : S)
이것은 첫 번째 접근과 매우 비슷하지만, 모듈이 S 라는 module type도 함께 노출한다는 점이 다르다. 이 누수 의 유용한 부수 효과는, module type of My_mod 를 작성하지 않고도 My_mod.S 로 모듈의 signature를 쉽게 참조할 수 있다는 점이다.
나는 이 분리가 매우 바람직하다 고 생각한다. 하지만 OCaml의 모듈 시스템은 매우 표현력이 높기 때문에, 몇 가지 영리한 인코딩을 통해 이 분리를 우회하는 것도 가능하다. 내 관점에서 이런 접근들은 주로 그 표현력 을 보여주는 데 의미가 있다. 모든 것을 한 파일에 합쳐 버릴 때의 단점은 분리 컴파일을 잃는 것인데, 나는 그것을 꽤 아쉬운 일 이라고 생각한다.
내가 이야기하고 싶었던 지점들을 짧게 나마 훑어본 것 같다. 내 관점에서 OCaml은 놀라운 언어다! 고급 타입 시스템, 풍부한 모듈 언어, 객체, 객체와 polymorphic variants를 통한 row polymorphism 지원, 그리고 사용자 정의 effects 덕분에, 안전성과 표현력 사이의 훌륭한 균형을 제공한다. 연구와 산업의 교차점에 있다는 점은, 내 생각에, 이 언어가 올바른 방향으로 진화하도록 만든다. 새로운 기능들을 너무 빠르거나 검증되지 않은 방식으로 도입하는 함정 없이, 현대성을 유지하기 위해 신중하게 통합해 가고 있기 때문이다.
몇 년 동안 OCaml의 도구들은 다소… 낡아 보였을 수도 있지만, 최근에는 몇몇 회사들의 상업적 지원 덕분에 도구들이 크게 현대화되었고, 플랫폼 로드맵에서 보듯 계속 개선되고 있다. 더불어 성장하는 라이브러리 생태계 덕분에, OCaml은 다양한 컴파일 타깃(예를 들어 js_of_ocaml과 wasm_of_ocaml을 통한 브라우저)을 바탕으로 매우 폭넓은 맥락에서 사용 가능하다.
표현력 있는 언어, 다재다능한 생태계, 지지적이고 반응이 빠른 커뮤니티가 결합되면서, OCaml은 개인 프로젝트와 직업 프로젝트 모두에 대해 매우 설득력 있는 선택지가 된다. 물론 전체 코드베이스를 OCaml로 마이그레이션하는 것은 아마도 실용적인 움직임이 아니겠지만, 작은 개인 프로젝트를 염두에 두고 있고 프로그래밍 언어를 재미있고 호기심 있게 바라본다면, 나는 진지하게 여러분이 OCaml을 고려해 보길 권한다!
내가 이 언어(그리고 그 생태계)에 대한 열정을 충분히 전달할 수 있었기를 바란다. 만약 이에 대해 이야기하고 싶거나, 프로젝트를 찾고 싶거나, 기여 기회를 탐색하고 싶다면, 나는 기꺼이 이야기할 것이다 — 또는 활발하고, 반응이 빠르며, 친절한 포럼을 통해 커뮤니티에 연락해도 좋다!