뷰 타입: region이 쉽게 만드는 난제
자신의 다른 필드를 가리키는 필드를 가진 값 —— 자신의 text로의 슬라이스를 쥔 문서 —— 은 언어 설계의 진짜 난제 중 하나로, Rust는 Pin과 unsafe를 요하고 다른 언어는 dependent types에 손을 뻗는다. Mere는 그것을 세 개의 작은 규칙으로 푼다. 풀 수 있는 것은, 앞 두 회에서 만든 region이 이미 어려운 부분을 짊어지고 있었기 때문이다.
앞 두 회는 모두, 같은 미뤄 둔 물음을 가리키며 끝났다: region 으로의 참조를 가진다는 것은 무엇을 뜻하며, 그것이 가리키는 곳보다 오래 사는 것을 어떻게 막는가? 계속 미뤄진 것은, 그 밑에서 이것이 언어 설계의 진짜 난제 중 하나이기 때문이다 —— Mere의 답이 얼마나 작아지는지를 보기 전에, 왜 어려운지를 봐 둘 가치가 있다.
자기 참조하는 값
한 번 파싱하고, 그 뒤로는 자신의 text로의 슬라이스를 쥐고 유지하는 문서를 생각하자:
struct Document { text: owned String }
// 바람: text를 한 번 파싱하고, 토큰을 그것으로의 슬라이스로 유지
view DocumentView of Document {
tokens: Slice<&self.text, str>, // self.text 안을 가리킨다
}
tokens는 Document.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로 선언”
뿐이다. 다른 언어에 Pin과 unsafe, 혹은 dependent type 시스템을 요하는 문제가, Mere
에는 세 개의 작은 규칙으로 족하다 —— 앞 회에서 만든 메모리 모델이, 이미 무게를 짊어지고
있었기 때문이다. 이것이, 두 개념을 하나로 접은 것이 하류에서 보답받는 모습이다: 다음의
어려운 기능이, 거의 공짜가 된다.
여기에는 언어가 몇 번이고 되돌아오는 사상적 지점이 있다. Rust의 자기 참조에 대한 정직한
답은 unsafe다 —— 보장에 뚫은 명시적 구멍이고, 정직하기는 하나, 구멍은 구멍이다. Mere는
차라리, 구멍이 필요 없어질 때까지 기능을 제약한다: 뷰는 immutable·region 결속·이동 불가일
수밖에 없고, 그 대신 탈출구 없이 검사된다. “무엇이든, 어디서든, unsafe로”보다 표현력은
낮고, 그것이 일부러 이뤄지는 거래다 —— 그것이 실제로 담는 사례(파스 트리, 문자열 인터닝,
문서와 그 인덱스)를, 타입에서 읽어낼 수 있는 보장과 함께 담는다.
뷰의 런타임 비용
거의 없다, 그리고 만족스러운 이유로. 뷰의 모든 필드가 region 내 참조이거나 프리미티브
라면, 뷰 전체가 그 region에서 Trivial이 된다 —— 지난 회의 제약이다. 그래서 뷰 자신은
정리를 요하지 않는다: region이 회수될 때, 거기의 다른 모든 것을 해제하는 것과 같은 포인터
한 번의 리셋으로 회수된다. 타입 레벨의 이야기와 런타임의 이야기가 맞아떨어진다 —— 뷰가
안전한 것과 공짜인 것은, 같은 이유, 즉 하나의 region과 생사를 함께한다는 데서 온다.
Part II는 Mere에 명시적이고 검사 가능한 메모리 모델을 주는 것을 목표로 했고, 뷰 타입으로
그 형태는 완성되었다: 다섯 전략을 통합 region으로 좁히고, region이 담을 수 있는 것과
with가 정리해야 할 것을 가르는 Trivial의 선, 그리고 이제, region 안에 머무름으로써
안전하게 만든 자기 참조. 남은 것은 그것을 쓸 수 있게 하는 것 —— 프로그램이 실제로 손을
뻗는 일상의 컨테이너다. 다음 회: region 대응 컬렉션, 그리고 어느 것이 어느 것 안에
중첩될 수 있는지를 정하는 Trivial 계층.