뷰 타입: region이 쉽게 만드는 난제

자신의 다른 필드를 가리키는 필드를 가진 값 —— 자신의 text로의 슬라이스를 쥔 문서 —— 은 언어 설계의 진짜 난제 중 하나로, Rust는 Pin과 unsafe를 요하고 다른 언어는 dependent types에 손을 뻗는다. Mere는 그것을 세 개의 작은 규칙으로 푼다. 풀 수 있는 것은, 앞 두 회에서 만든 region이 이미 어려운 부분을 짊어지고 있었기 때문이다.

merememory-modelview-typesregionslanguage-design

앞 두 회는 모두, 같은 미뤄 둔 물음을 가리키며 끝났다: region 으로의 참조를 가진다는 것은 무엇을 뜻하며, 그것이 가리키는 곳보다 오래 사는 것을 어떻게 막는가? 계속 미뤄진 것은, 그 밑에서 이것이 언어 설계의 진짜 난제 중 하나이기 때문이다 —— Mere의 답이 얼마나 작아지는지를 보기 전에, 왜 어려운지를 봐 둘 가치가 있다.

자기 참조하는 값

한 번 파싱하고, 그 뒤로는 자신의 text로의 슬라이스를 쥐고 유지하는 문서를 생각하자:

struct Document { text: owned String }

// 바람: text를 한 번 파싱하고, 토큰을 그것으로의 슬라이스로 유지
view DocumentView of Document {
  tokens: Slice<&self.text, str>,   // self.text 안을 가리킨다
}

tokensDocument.text 안을 가리킨다. 이것은 바로 파서가 원하는 형태다 —— 버퍼를 토큰화하고, 부분 문자열을 복사하는 대신 값싼 슬라이스를 들고 다닌다 —— 그리고 지뢰밭 이기도 하다. Document가 움직인 순간, text는 새 주소에 앉고, tokens 안의 모든 슬라이스가 dangling이 된다. 타입 시스템은 “같은 값의 다른 필드로의 참조”를 말할 수 있어야 하고, 구축은 text를 먼저, 슬라이스를 나중에 만들어야 하며, 파기는 슬라이스를 text보다 먼저 drop하지 않으면 부정한 상태를 지나게 된다.

이것은 구석 사례가 아니라 유명한 사례다. Rust는 Pin<T>로 값을 제자리에 고정하고, 실제로 자기 참조를 만들려면 unsafe를 요구한다. dependent types를 가진 언어는 정확히 표현할 수 있지만, 대부분의 프로그램이 치러선 안 될 대가를 동반한다. 문제는 진짜이고, 통상의 해법은 무겁다.

세 공리

Mere의 답은 일부러 작다 —— 세 규칙, 그리고 unsafe 없음:

A —— 뷰는 immutable한 묶음이다. 뷰는 구축 시점에 내부의 참조 관계를 확정한다. 그 뒤 로는 변경도 이동도 못 하고, 불변 참조를 통해서만 쓰인다. 어느 필드의 주소도 변할 수 없으 므로, 그리로의 슬라이스가 낡을 일이 없다.

B —— 뷰는 region 안에서 구축한다. 뷰는 허공에 뜬 heap이 아니라 region 위에 산다. 그 lifetime은 region의 lifetime 그 자체이고, 모든 필드가 같은 region을 가리키므로, 필드 간 참조는 모두, 하나의 운명을 함께하는 한 영역의 내부에서 닫힌다.

C —— view V[R] of T는 진짜 구문이다. 뷰는 struct의 파생형으로, 사는 region으로 파라미터화하여 선언한다. 파라미터 R은 타입 시스템이 소리 내어 “이것 전부가 region R 안이다”라고 말하는 수단이다 —— 제약은 당신의 머리가 아니라 타입 안에 있다.

적어 보면, 뷰와 그 생성자는 평범한 코드다:

view DocumentView[region R] of Document {
  own:    &R Document,        // Document 본체, region R에 놓인다
  tokens: &R [&R str],        // own.text 안을 가리키는 슬라이스
}

fn make_view[region R](src: &borrowed str, r: &R Region) -> &R DocumentView[R] {
  let doc    = r.alloc(Document { text: copy_to_region(src, r) })
  let tokens = tokenize(doc.text, r)     // doc.text로의 슬라이스, 같은 region
  r.alloc(DocumentView { own: doc, tokens: tokens })
}

새 구축 구문도, 탈출구도 없다 —— 그저 region으로의 할당과, 평범한 레코드다.

왜 안전한가, 그리고 그것이 우연이 아닌 이유

자기 참조가 통상 어떻게 깨지는지를 하나씩 따라가면, 그 모두가, 당신이 이미 만난 규칙으로 닫혀 있다:

  • 값은 움직일 수 없다(공리 A). 그래서 슬라이스가 가리키는 주소는 결코 변하지 않는다.
  • 변경할 수 없다(공리 A). 그래서 구축 시 짠 관계가 나중에 깨질 수 없다.
  • 모든 필드가 같은 region을 가리키고(공리 B), region은 한꺼번에 해제한다(두 회 전의 통합) —— 그래서 슬라이스가 가리키는 곳이 슬라이스보다 먼저 해제될 일이 없다. 파기에서 지날 부정한 중간 상태가 없다. 영역째가 한 수로 사라진다.
  • 뷰는 region 밖으로 내갈 수 없다(타입 안의 R, 공리 C). 그래서 그것이 가리키는 곳보다 오래 사는 경로가 없다.

두드러지는 것은, 이 중 얼마나 적은 부분이 뷰에 관한 것인가다. 안전성의 거의 전부가, region이 이미 가지고 있던 성질에서 온다: 움직이지 않는다, 한꺼번에 해제한다, 그리로의 참조는 R로 타입 지어진다. 뷰의 규칙이 더하는 것은 “immutable, 그리고 of T로 선언” 뿐이다. 다른 언어에 Pinunsafe, 혹은 dependent type 시스템을 요하는 문제가, Mere 에는 세 개의 작은 규칙으로 족하다 —— 앞 회에서 만든 메모리 모델이, 이미 무게를 짊어지고 있었기 때문이다. 이것이, 두 개념을 하나로 접은 것이 하류에서 보답받는 모습이다: 다음의 어려운 기능이, 거의 공짜가 된다.

여기에는 언어가 몇 번이고 되돌아오는 사상적 지점이 있다. Rust의 자기 참조에 대한 정직한 답은 unsafe다 —— 보장에 뚫은 명시적 구멍이고, 정직하기는 하나, 구멍은 구멍이다. Mere는 차라리, 구멍이 필요 없어질 때까지 기능을 제약한다: 뷰는 immutable·region 결속·이동 불가일 수밖에 없고, 그 대신 탈출구 없이 검사된다. “무엇이든, 어디서든, unsafe로”보다 표현력은 낮고, 그것이 일부러 이뤄지는 거래다 —— 그것이 실제로 담는 사례(파스 트리, 문자열 인터닝, 문서와 그 인덱스)를, 타입에서 읽어낼 수 있는 보장과 함께 담는다.

뷰의 런타임 비용

거의 없다, 그리고 만족스러운 이유로. 뷰의 모든 필드가 region 내 참조이거나 프리미티브 라면, 뷰 전체가 그 region에서 Trivial이 된다 —— 지난 회의 제약이다. 그래서 뷰 자신은 정리를 요하지 않는다: region이 회수될 때, 거기의 다른 모든 것을 해제하는 것과 같은 포인터 한 번의 리셋으로 회수된다. 타입 레벨의 이야기와 런타임의 이야기가 맞아떨어진다 —— 뷰가 안전한 과 공짜인 것은, 같은 이유, 즉 하나의 region과 생사를 함께한다는 데서 온다.

Part II는 Mere에 명시적이고 검사 가능한 메모리 모델을 주는 것을 목표로 했고, 뷰 타입으로 그 형태는 완성되었다: 다섯 전략을 통합 region으로 좁히고, region이 담을 수 있는 것과 with가 정리해야 할 것을 가르는 Trivial의 선, 그리고 이제, region 안에 머무름으로써 안전하게 만든 자기 참조. 남은 것은 그것을 쓸 수 있게 하는 것 —— 프로그램이 실제로 손을 뻗는 일상의 컨테이너다. 다음 회: region 대응 컬렉션, 그리고 어느 것이 어느 것 안에 중첩될 수 있는지를 정하는 Trivial 계층.

← Back to Mere: 언어를 만들다