OxCaml의 하위 호환성 약속이 언어 설계를 어떻게 복잡하게 만들었는지, 그리고 더 단순한 대안 경로가 가능했는지를 살펴본다.
하위 호환성의 대가
2026년 4월 24일 · 18분 읽기
OxCaml은 Jane Street가 OCaml을 산화 하려는 노력이다. 즉, 더 Rust답게 만들려는 시도다. 특히 여기서 주목하는 Rust의 측면은 컴파일러가 데이터 경쟁을 방지할 수 있게 해 주는 “겁 없는 동시성”이며, 이는 OCaml 5의 새로운 멀티코어 기능을 고려할 때 중요하다.1
이 프로젝트는 언어에 상당히 많은 흥미로운 새 기능들(모드, 종류, 레이아웃 등)을 추가하지만, 오직 언어의 “확장”으로만 작동하려고 조심스럽게 접근하며, 첫 화면에서 다음과 같이 약속한다.
모든 유효한 OCaml 프로그램은 또한 유효한 OxCaml 프로그램이다
나는 이 약속이, 비록 고결하긴 하지만, 결국에는 잘못된 방향이었다고 주장하려 한다. 그것이 더 단순할 수도 있었던 언어를 포기하도록 강제했기 때문이다. 이를 위해 먼저 OxCaml을 조금 살펴보고, 그것이 어떻게 복잡한지 이야기하고, 실제 사용에서 어떠한지 검토한 뒤, 마지막으로 대안 경로를 탐색해 보겠다. 이 글을 다 읽고 나면, 이 약속에도 불구하고 왜 Core validation library가 다음과 같이 생긴 모습에서
type result
type 'a check = 'a -> result
val combine : result -> result -> result
val of_list : result list -> result
val name : string -> result -> result
val name_list : string -> result list -> result
val fail_fn : string -> _ check
val pass_bool : bool check
val pass_unit : unit check
val try_with : (unit -> unit) -> result
val first_failure : result -> result -> result
대신 이렇게 바뀌게 되었는지 이해하게 되길 바란다. (간결함을 위해 주석은 생략했다)
type result : value mod contended
type%template ('a : any) check = 'a -> result @ p [@@mode p = (portable, nonportable)]
val%template combine : result @ p -> result @ p -> result @ p [@@mode p = (portable, nonportable)]
val%template of_list : result list @ p -> result @ p [@@mode p = (portable, nonportable)]
val%template name : string -> result @ p -> result @ p [@@mode p = (portable, nonportable)]
val%template name_list : string -> result list @ p -> result @ p [@@mode p = (portable, nonportable)]
val%template fail_fn : string -> (_ check[@mode p]) @ portable [@@mode p = (portable, nonportable)]
val%template pass_bool : (bool check[@mode p]) [@@mode p = (portable, nonportable)]
val%template pass_unit : (unit check[@mode p]) [@@mode p = (portable, nonportable)]
val%template try_with : (unit -> unit) @ p -> result @ p [@@mode p = (portable, nonportable)]
val%template first_failure : result @ p -> result @ p -> result @ p [@@mode p = (portable, nonportable)]
그렇다면 위에서 보고 있는 “mode”라는 단어는 무엇일까? OxCaml의 모드 시스템은 일종의 타입 시스템과 비슷하다. 즉, 모든 값 x : t @ m은 정적으로 알려진 어떤 타입 t와 모드 m을 가지며, 여기서 @는 모드 주석 문법이다.
모드는 깊은 성질이다. 따라서 m은 x의 내용물에 재귀적으로 적용된다. 또한 모드는 서로 대체로 직교하는 여러 모드 축들의 격자로 구성되며, 일반적으로 타입과도, 서로 간에도 직교한다. (다만 일부 타입의 일부 값은 특정 모드를 무시하거나 “가로지를” 수 있다.) 이를 구체적으로 만들기 위해, 이것들이 Rust의 타입 시스템에 어떻게 대응할 수 있는지 몇 가지 예를 살펴보자.
Rust의 핵심에는 borrow checker가 있다. 이것은 각 값 x: T가 하나의 소유자를 가지도록 강제하지만, 그것이 일시적으로 &x: &T 또는 &mut x: &mut T로 빌려질 수 있도록 허용하며, 이러한 빌림은 타입 시스템에 반영된다. 그렇다면 OxCaml에서는 이와 비슷한 것을 어떻게 표현할 수 있을까?
이 경우, 이를 반영하기 위해 값의 타입을 바꾸도록 강제되는 대신, 그 값을 어떻게 사용할 수 있는지에 대한 이 추가 정보를 모드에 넣는다.
&x: &T는 대략 x : t @ local read aliased에 대응할 수 있다x: T는 대략 x : t @ global read_write unique에 대응할 수 있다여기서 우리는 첫 세 가지 모드 축을 본다.
local은 스코프를 벗어날 수 없는 값을 나타내고, global 값은 그럴 수 있다. (Rust의 lifetime과 유사하다.)read는 변경 가능한 상태를 읽을 수는 있지만 쓸 수는 없는 값을 나타내고, read_write 값은 둘 다 가능하다.2aliased는 다른 참조가 있을 수 있는 값을 나타내고, unique 값은 Rust의 affine type system처럼 동작한다.지금까지의 이 모드들에 대해서는 나는 비교적 호의적이다. 이들은 각각이 무엇을 제어하는지 명확성을 제공하며, 단일한 소유 개념 안에 뒤섞지 않는다. 또한 Rust의 방식처럼 간접 참조를 가진 참조 여부와 떼려야 뗄 수 없이 얽힌 형태로 x의 타입을 오염시킬 필요도 없다.3 하지만 독립적으로 설정될 수 있는 축의 수가 늘어날수록 가능한 상태가 일종의 조합 폭발을 일으킬 위험이 있다는 점도 이미 보일 것이다.4 각 값이 타입, 지역성, 가시성, 유일성을 모두 가질 수 있다면, 평소보다 훨씬 더 많은 것을 머릿속에 담고 있어야 한다.
OxCaml에는 Rust의 소유 개념에 그렇게 직접적으로 대응하지는 않지만, 대신 Rust라면 트레이트에 넣었을 법한 모드도 있다. 특히 여기서 집중하고 싶은 것은 portability 모드다.
portable은 다른 스레드와 공유될 수 있는 값5을 나타내고, nonportable 값은 그럴 수 없다.이식성은 Rust의 Send와 Sync 트레이트에 비유할 수 있다. 이들은 어떤 타입이 스레드 간에 안전하게 공유될 수 있는지를 결정한다.6
여기서 중요한 구분점이 있다. Rust에서의 개념은 타입의 성질이었다. T: Send + Sync라면, 타입 T의 어떤 값이든 스레드 사이에서 공유될 수 있었다. 반면 OxCaml에서는 이것이 값의 성질이다. 같은 타입을 가지더라도 서로 다른 이식성을 갖는 x1 : t @ portable와 x2 : t @ nonportable 같은 값을 가질 수 있다.
이 점은 처음에는 Rust 프로그래머에게 직관적이지 않을 수 있다. Rust 프로그래머는 타입이 스레드 안전성을 결정한다고 생각하는 데 익숙하기 때문이다. 전형적으로 원자적 참조 계수를 사용하는 Arc<_> 값은 공유될 수 있지만, 비원자적인 Rc<_> 버전은 그렇지 않다. 하지만 여기서 기억해야 할 중요한 점은, OCaml에서는 함수들이 모두 같은 화살표 타입의 값이라는 것이다.
val func : t1 -> t2
그리고 가능한 함수들의 공간은 매우 넓다. 여기에는 분명히 스레드 간 공유가 가능한 함수들도 있고, 가변 상태를 캡처하여 여러 스레드에서 호출될 경우 데이터 경쟁의 위험이 있는 클로저들도 포함된다.
즉, 화살표 타입에는 그 값 전체에 공통으로 적용할 하나의 결정적인 이식성을 부여할 수 없다.7 더 나아가, 어떤 추상 타입이든 잠재적으로 함수를 포함할 수 있기 때문에, 명시적으로 함수를 포함하지 않아 portability를 “가로지른다”고 주석된 경우를 제외하면 사실상 거의 모든 OCaml 값은 어떤 이식성 상태를 유지해야 한다.
OxCaml은 현재 문서화된 9개의 모드를, 앞서 본 것과 같은 4개의 “과거” 모드와, 이식성과 같은 5개의 “미래” 모드로 나눈다. 후자는 이와 비슷한 이유로 함수를 포함하는 타입에만 관련된다.
앞서 OxCaml이 모든 OCaml 코드와의 하위 호환성을 약속한다고 했다. 여기서 즉시 드는 관찰은, 기존 OCaml 코드에는 곳곳에 모드 주석이 없었으므로, OxCaml은 모드를 명시하지 않는 코드도 처리할 수 있어야 한다는 점이다. 이를 위해 OxCaml은 “기본 모드”를 사용한다.
각 모드 축에는, 모드가 명시적으로 주석되지 않았을 때 가정되는 기본 위치가 있다. 이 기본값들은 하위 호환성을 깨뜨리지 않도록 신중하게 선택되어야 한다.
global이어야 한다.read_write여야 한다.aliased여야 한다.대체로 지금까지는 모든 모드를 최대한 허용적인 방향으로 설정해 왔다. 즉, 값에 대해 원하는 대로 할 수 있도록 해 왔다.8 이제 이것을 portability 모드로 확장할 때 어떻게 해야 할지 생각해 보자.
기존 OCaml 코드는 언제나 단일 코어였고9, 따라서 스레드 간 공유 가능성에 대한 기존 개념 자체가 없었다. 그러니 계속 허용적인 모드를 사용할 수 있고, 기본적으로 값을 스레드 간에 공유 가능하게 두면 좋을 것처럼 보인다.
하지만 그렇다고 우리가 원하는 아무 기본값이나 자유롭게 정할 수 있다는 뜻은 아니다. 우리는 또 다른 약속을 했기 때문이다. OxCaml이 데이터 경쟁을 정적으로 방지하겠다고 말한 것이다. 그리고 앞서 논의했듯, 스레드 간에 공유되면 실제로 데이터 경쟁을 일으킬 수 있는 값들이 존재한다. 따라서 우리는 다음과 같이 설정해야 한다.
nonportable하다고 가정해야 한다.안타깝게도 이번에는 더 제한적인 기본 모드를 선택해야 한다. 그리고 기본 모드는 모든 기존 OCaml 코드에 적용되므로, 이는 OxCaml에서 사용할 때 모든 OCaml 코드가 스레드 안전하지 않을 수 있다고 추정되어야 함을 의미한다.10 이것은 코드를 작성할 때 꽤 큰 고통을 만들어 낼 것이다.
그렇다면 모든 기존 OCaml 코드가 nonportable하다고 말하는 것은 무엇을 의미할까? 정의상 그것은 그런 값들이 스레드를 넘을 수 없다는 뜻이지만, 그 이상의 함의도 생각해 보자.
OxCaml이 하위 호환성 이야기와 함께 자주 강조하는 또 하나는 복잡성의 “pay-as-you-go” 특성이다. 즉, 어떤 특정한 고급 새 기능이 필요 없다면 그 기능을 그냥 피하고 평범한 옛 OCaml을 계속 쓰면 된다는 주장이다. 과연 실제로 그렇게 될 수 있을까?
이 말을 문자 그대로 받아들인다면, 우리는 코드 대부분을 OCaml로 작성하고, OxCaml은 그것을 nonportable로 해석하게 둘 수 있다. 그런 다음 성능 확장이 중요한 앱에 필요할 때만 OxCaml의 동시성 기능과 그에 따른 복잡성을 받아들이고 싶을 것이다.
이 세계에서는, 새 앱을 쓰거나 기존 앱을 portable 동시성 OxCaml로 옮길 때도 더 큰 OCaml 생태계와 기존 라이브러리들을 계속 활용할 수 있기를 기대할 수 있다. 하지만 불행히도, 그것은 매우 어렵다는 것이 드러난다.
비이식성은 전염된다. 앞서 모드는 깊다고 했다. 즉, 값의 모든 내용에 재귀적으로 적용된다. 따라서 portable 값은 어떤 nonportable한 것도 포함할 수 없고, nonportable 함수를 호출하는 함수는 그 자체도 nonportable해야 한다.
기본 모드의 모든 값이 nonportable하다는 점을 고려하면, nonportable한 기존 OCaml에 의존하는 portable 동시성 OxCaml을 작성하라고 요구하는 것은, 모든 함수가 async인 라이브러리에 의존하는 동기식 코드를 작성하라고 요구하는 것과 비슷하다. 그냥 작동하지 않는다!
하지만 그렇다고 진정한 하위 호환성의 희망이 완전히 사라지는 것은 아니다. 반대 방향에는 여전히 한 줄기 희망이 남아 있다. OxCaml에 의존하는 OCaml 코드를 쓰고자 할 경우다. nonportable 함수는 portable 함수를 호출할 수 있다. 이것은 OCaml과 OxCaml이 공존할 수 있는 경로를 열어 준다.11
portable OxCaml과 nonportable OCaml이 의존성을 공유하게 하려면, 우리는 의존성 스택의 가장 바닥부터 시작해야 한다. 즉, 아직 어떤 기존 nonportable 코드에도 의존하지 않는 곳에서 시작해야 한다. 그 라이브러리를 portable OxCaml로 바꾸는 것은 안전하다. 기존 OCaml 코드는 여전히 그것을 사용할 수 있고, 이제 portable 코드도 사용할 수 있게 되기 때문이다.
이 작업을 하고 나면, 이제 몇몇 다른 라이브러리도 옮길 수 있는 길이 열린다. 왜냐하면 그것들이 이제 오직 portable 라이브러리에만 의존하게 되었고, 따라서 그들 자신도 이식 가능해질 수 있기 때문이다. 점차적으로 우리는 이 마이그레이션의 범위를 전체 생태계로 넓혀 갈 수 있다. 이 과정을 portabilization이라 부르자.
이 마이그레이션을 수행하는 동안, 우리는 사실상 두 가지 상태 중 하나에 있게 된다.12
물론 전자는 단순하지만, 후자는 실제 작업을 만들어 낼 수 있다. 특히 우리가 OxCaml로 가장 옮기고 싶어 하는, 성능이 중요한 코드들을 포팅할 때 그렇다. 그런 코드는 강한 변경 가능성을 사용할 가능성이 가장 높기 때문이다.13
그래도 우리는 터널 끝의 빛을 볼 수 있기에 계속 나아간다. 모든 라이브러리를 하나씩 OCaml에서 OxCaml로 옮길 수 있다면, 마침내 언어 간 상호운용이 가능해질 것이다. 즉, portable 방식이든 nonportable 방식이든 코드를 작성하는 선택을 하면서도 같은 의존성을 공유할 수 있게 된다. 한 번에 모두 마이그레이션할 수 없는 대량의 기존 OCaml 코드를 가진 Jane Street 같은 조직에게는 이것이 필수적이다.14
그런데 여기서 내가 약간 눈속임을 한 것처럼 느껴지지 않는가? 나는 이 글의 처음에 언어가 완전히 하위 호환되며, 아무 수정 없이 모든 옛 코드를 컴파일할 수 있다고 이야기했다. 그런데 이제는 모든 라이브러리를 적극적으로 OCaml에서 OxCaml로 마이그레이션해야 하는 세계를 설명하고 있다.
그리고 그렇게 해서 모든 라이브러리가 portable OxCaml에 들어오고, 그 위에 의존하는 앱들을 어떤 방식으로 쓸지 자유롭게 선택할 수 있게 된다고 하자. 그렇다면 우리는 어느 쪽을 선택할까? 아무도 우리에게 의존할 수 없고, 미래에 또 마이그레이션해야 할지도 모르는 쪽일까? 아닐 것이다. 아마 우리는 이 새로운 portable OxCaml에 머물게 될 것이다.
그렇다면 OCaml 생태계에 OxCaml을 도입하는 일은 사실상 언어 전체를 마이그레이션하는 것처럼 느껴질 위험이 있다. 모드와 레이아웃을 위한 낯선 새 문법에 익숙해져야 한다. 심지어 가변 데이터 위의 클로저가 제한적인 새로운 언어의 패러다임에 맞추기 위해 코드 일부의 논리도 상당히 다시 써야 한다.
Python 3 마이그레이션만큼 나쁘지는 않지만15, 이 하위 호환성 약속은 우리가 보통의 언어 진화에서 기대하는 것과는 조금 달랐다. 보통은 옛 코드를 거의 건드리지 않아도 되기 때문이다. 마치 언어를 마이그레이션하는 것 같지만, 특별한 성질 하나가 있다. OCaml 코드는 OxCaml 코드에 의존할 수 있고, 둘은 같은 툴체인을 공유할 수 있다는 점이다.
어느 정도는 이것이 불가피했다. 실제로 스레드를 넘지 않는다는 사실 덕분에만 안전했던 OCaml 코드가 많이 있었다. 그런 코드를 수정 없이 멀티코어 언어로 마법처럼 옮길 수는 없었다. 그래도 우리가 할 수 있는 것이 결국 그런 종류의 마이그레이션뿐이라는 점을 인정한다면, 고려할 설계 공간을 더 넓힐 수도 있지 않을까.
그렇다면 이 말을 정말 그대로 받아들인다면 어떨까? 스레드 안전성을 요구하는 언어와 오직 단일 스레드만 사용하던 언어 사이에 진정한 하위 호환성은 결코 완전히 달성할 수 없다고 판단한다면? 우리가 기대할 수 있는 최선이란, 컴포넌트를 점진적으로 교체하고 여전히 OCaml 코드를 사용할 수 있다는 점 때문에 OCaml로부터의 마이그레이션 경로가 존재하는 것뿐이라고 결론 내린다면?
이런 종류의 변화는 Rust의 edition 개념과 비슷하게 느껴질 수 있다. 프로젝트가 자신이 어떤 언어 edition으로 작성될지를 결정하고, 그 edition들은 서로 반드시 하위 호환적일 필요는 없다. 하지만 여전히 호환되는 인터페이스를 유지하고, 심지어 한쪽에서 다른 쪽으로 바꾸는 자동 마이그레이션 도구도 제공한다.16
그렇다면 우리는 스레드 안전한 후속 edition을 어떻게 설계할까? OxCaml 같은 것을 쓰게 될까? 내 주장은 그렇지 않다는 것이다. 기본 모드의 설정에서 보았듯, 이 언어가 평범한 OCaml을 컴파일할 수 있어야 한다는 제약은 우리에게 정말 많은 제한을 부과했다. 그리고 그 제한은 복잡성을 낳았다. 여기서 집중한 모드의 조합 폭발뿐 아니라, unboxed type, layout, 그리고 다른 OxCaml 기능들을 사용하는 방식에서도 마찬가지다.
가능한 언어의 공간은 매우 넓고, 내가 여기 완벽한 버전을 들고 왔다고 주장할 생각은 없다. 그래도 최소한 우리가 논의한 문제점들을 더 나은 가상의 OxCaml에서 어떻게 개선할 수 있을지 몇 가지는 이야기해 보겠다.
멀티스레딩을 위해 설계된 언어라면, portable이 기본 모드여야 하며, nonportable 값은 예외처럼 느껴져야 한다. 이는 특히 불변성이 기본인 함수형 언어에서는 더욱 그렇지만, 그 밖의 경우에도 마찬가지다. Rust는 static mut를 힘들게 unsafe로 감싸도록 요구하며, OCaml에서도 이런 종류의 것은 비슷하게 억제되어야 한다. 대신 지금은 이런 종류의 전역 공유 가변 상태가 언어에 의해 쉽고 관용적인 것처럼 보이게 된다.
Jane Street 핵심 라이브러리의 OxCaml 브랜치들을 살펴보면, 사실상 이미 대부분의 파일이 @@ portable로 시작하여 모듈 전체 내용을 portable로 간주해야 함을 나타내고 있다. 그래도 선언을 어떻게 해석해야 하는지 알기 위해 맨 위로 스크롤해야 하는 상황은 원하지 않는다. 이것이 합리적인 방식이라면, 전반적으로 그렇게 적용되어야 하며, nonportable 값이야말로 자신의 존재를 명시적으로 알려야 한다고 나는 주장한다.
여기서 마이그레이션 도구는 모든 값을 nonportable로 표시할 수도 있고, 혹은 컴파일러가 자동으로 portable하다고 추론할 수 없는 값만 그렇게 표시할 수도 있다. 모드가 지워지는 기존 OCaml edition 의존성에는 물론 아무 영향도 없을 것이다. 여전히 스레드를 건넌다는 개념 자체가 없기 때문이다.
하지만 더 나은 세계도 상상할 수 있다. 단지 합리적인 기본값이 있는 세계가 아니라, portability 모드 자체를 완전히 없앨 수 있는 세계다. 그러려면 스레드 안전한 함수와 그렇지 않은 함수의 차이를 타입 시스템에 표현해야 한다. 다소 Rust의 Fn, FnMut, FnOnce처럼 말이다.
본질적으로 nonportable한 함수는 자신의 클로저에 저장된 값을 변경하는 함수이며, 이것이 데이터 경쟁을 일으킬 수 있다. 이를 다른 방식으로 말하면, 그런 함수는 호출되기 위해서 자신의 클로저 내용에 대해 가변성 모드에서 read_write17 접근권을 가져야 하는 함수다. 보통 모드는 깊어서 값의 내용에 재귀적으로 적용된다. 함수의 클로저는 우리가 이를 추적하거나 강제하지 않는 유일한 예외다.
이것을 함수의 타입에 표현하는 것을 상상할 수 있다. type (-in, +out) fn을 가지는 대신, OxCaml이 type (-in, +out, +mode) fn을 가질 수 있다고 해 보자. 여기서 mode는 그 함수를 호출할 수 있도록 허용되기 위해 그 함수를 어떤 모드로 보유해야 하는지를 나타낸다.18 그러면 클로저 내용물에 대한 데이터 경쟁은 다른 곳의 데이터 경쟁을 막는 것과 같은 메커니즘으로 처리할 수 있다. 마이그레이션은 함수들에 대해 보수적인 요구사항을 가정하면 된다.
그러면 갑자기 미래 모드들을 거의 잘라낼 수 있게 된다. 물론 이것 역시 나름의 조합 폭발 위험을 낳을 수 있지만, 훨씬 더 제한된 범위의 문제다. 이런 매개변수는 모든 값에 붙는 모드와 달리 함수에만 적용되며, 대부분의 경우 아마 그 모두에 대해 제네릭할 필요도 없고, 특정 사용 사례에서 어떤 종류의 함수가 허용되어야 하는지 어느 정도 기대할 수 있을 것이다.
더 많은 모드를 없애거나 OxCaml의 다른 기능들을 단순화하기 위한 훨씬 더 과감한 제안들도 상상할 수 있다. 심지어 나중에 덧붙인 모드가 아니라 linear type이나 affine type을 기본으로 채택할 수도 있다. 실제로 선형 논리는 우리의 여러 문제를 단순화해 주기 때문이다. 이런 가능성을 전부 열거하려 하지는 않겠다.
이쯤 되면 내가 마치 OCaml은 사라져야 하며, Rust 같은 기능을 받아들일 거라면 아예 끝까지 가야 한다고 말하는 것처럼 들릴 수도 있다. 나는 그것을 믿지 않는다는 점을 분명히 하고 싶다. 나는 OCaml이 특히 우아한 언어라고 생각하며, 바로 그 이유 때문에 그 능력을 늘리고 싶어 하면서도 복잡성을 추가하는 데에는 주저한다.
Jane Street가 언어를 통제하지 못하고 항상 상류에 자신들의 변화를 끌고 갈 수 없는 상황에서, 하위 호환성 없는 제대로 된 edition을 실질적으로 제공하기 어려웠을 수도 있다. 그래도 이런 변화들 중 일부가 상류로 되돌아간다면, 그러한 선택들을 검토하고 가능한 최선의 방식으로 이 힘을 추가할 기회가 생길 것이다.
나는 OCaml이 안전한 동시성을 제공하는 방법에 대해 다른 언어들의 경험에서 가능한 한 많이 배워야 한다고 믿는다. 그렇게 하려면 새로운 문법이 필요할 수 있다는 점은 이해할 수 있다. 하지만 이미 그 경우라면, 문법을 단지 명목상으로만 기존 것의 정확한 상위 집합으로 유지하는 것이, 필요 이상으로 더 복잡하게 만드는 대가를 치를 만한 가치는 없다고 생각한다.
OxCaml에는 표준적인 한 단어 option 표현을 넘어서 여러 layout을 가진 타입을 다룰 수 있는 능력 같은, 다른 Rust 유사 기능들도 있지만, 이 글에서는 거기에 집중하지 않겠다. ↩
변경 불가능한 상태 부분에만 접근할 수 있는 값을 위한 immutable 모드도 존재하는데, 모든 필드가 잠재적으로 변경 가능할 수 있는 Rust에는 이에 진정으로 대응하는 것이 없다. ↩
Rust에서는 메모리 정리 책임을 포인터 보유 여부와 분리할 수 있는 &own 참조에 대한 제안이 반복적으로 있었고, 비슷한 종류의 지저분함도 존재해 왔다. ↩
그 폭발은 우리가 예시에서 본 ppx_template에 의해 실제로 구현되기까지 하는데, 이것은 정말로 서로 다른 모든 모드 조합으로 확장된다. ↩
여기서 정확한 용어는 사실 “domain”일 텐데, 이것은 OCaml의 green-thread 버전이다. 하지만 이 글에서는 OxCaml 문서의 많은 부분이 그러하듯 “thread”라는 용어를 계속 혼용하겠다. ↩
Send와 Sync는 구별되는 것처럼 느껴질 수 있지만, 대략 값이 자신의 스레드에 독점적인지를 포착하는 또 다른 “contention” 모드를 추적함으로써 둘을 동시에 포착할 수 있다. ↩
반면 Rust에서는 각 클로저가 자기만의 익명 타입을 가지므로, 모든 클로저가 같은 Send와 Sync 상태를 공유할 필요가 없다. ↩
더 형식적으로 말하면, 모드 축에는 submoding 개념이 포함되어 있고, 우리는 언제나 다른 어떤 모드로도 자유롭게 이동할 수 있는 최소 모드를 선택해 왔다. ↩
여기서 나는 “기존 OCaml”을 OCaml 4로 보고 있으며, OxCaml은 그것으로부터의 잠재적 업그레이드라고 간주한다. OCaml 5는 이러한 안전 기능 없이 멀티코어를 도입하지만, 그 경우에 대해 OxCaml은 같은 하위 호환성 약속을 하지는 않는다. ↩
이것은 OCaml이 .ml 구현과 분리된 명시적 .mli 인터페이스를 사용한다는 점 때문에 더욱 악화된다. 즉, 컴파일러가 어떤 값들이 portable하다고 추론할 수 있더라도, 주석 없이는 그것을 외부에 드러낼 수 없다. ↩
여기에는 OxCaml 코드가 반드시 portable하다는 암묵적 가정이 있다는 점을 안다. 엄밀히 필수는 아니지만, 멀티코어 기능을 포기할 생각이라면 차라리 일반 OCaml을 쓰는 편이 낫다고 나는 주장한다. ↩
좋아, 사실은 3가지 상태다. 값은 portable하지만 컴파일러가 그것을 증명할 수 없어서 여기저기에 Basement.Stdlib_shim.Obj.magic_portable를 뿌리고 있을 가능성도 있다. ↩
실제로 어떤 집단에서는 단일 전역 값을 미리 할당해 두고, 비재귀 함수들에서 그것을 현재 데이터를 담도록 변경하는 것이 흔한 관행이었다. 이렇게 하면 가비지 컬렉션 사이클을 유발할 수 있는 할당을 피할 수 있었다. ↩
범위가 훨씬 더 작은 마이그레이션조차도 기존 시스템을 교란하지 않도록 조심해야 한다. 예를 들어 https://signalsandthreads.com/swapping-the-engine-out-of-a-moving-race-car/를 보라. ↩
여기서 Guido Van Rossum은 유명하게도 정반대의 약속을 했다
Python 2.6 코드는 Python 3.0에서 수정 없이 실행되어야 한다는 요구사항이 없다. 부분집합조차도 아니다. (물론 아주 작은 부분집합은 있겠지만, 주요 기능이 빠져 있을 것이다.)
↩
16. 인정하자면 Rust의 edition은 양방향 의존성을 허용한다는 점에서 다소 더 강한 보장을 제공한다. 이것은 Pin type 같은 것들에 대해서는 또 다른 고통을 만든다. ↩