Trivial, 그리고 region이 담을 수 없는 값
region은 포인터를 밀어 영역째 한꺼번에 해제한다 —— 그것이 건전한 것은 정리가 필요 없는 값뿐이다. 하지만 DB 연결도 파일도 소켓도 닫아야 한다. Mere의 답은 책임의 깔끔한 분할이었다: region이 메모리를 관리하고, `with`가 정리를 관리한다. 그리고 둘을 가르는 제약에는 이름이 있다 —— Trivial.
지난 회는 arena와 region을 하나의 개념으로 접고, 날카로워진 물음을 책상에 남겼다: region에는 실제로 무엇이 살 수 있는가? region은 포인터를 밀어 할당하고, 블록이 끝나면 영역째 한 수로 버린다. 빠른 것은 바로, 값마다의 정리를 돌리지 않기 때문이다 —— 그저 포인터를 리셋한다. 즉, 정리가 필요 없는 값에만 건전하다.
많은 값이 거기에 들어맞지 않는다. 데이터베이스 연결은 닫고, 열린 트랜잭션을 abort해야 한다. 파일에는 해제할 fd가 있다. 버퍼 로거는 flush가 필요하다. 소켓은 세션을 닫아야 한다. 이 값들은 메모리 바깥의 무언가를 소유하고 있어서, 값이 앉은 메모리를 해제해도 그것은 해제되지 않는다. 포인터를 리셋하면 연결을 흘리고 —— 더 나쁜 일도 생긴다.
즉 region 모델은, 흥미로운 자원이 바로 있는 곳에 구멍을 가진다. 이번 회는 그 구멍에 이름을 주는 제약과, 구멍을 메우는 장치의 이야기다.
Trivial: 밀고 잊는 것의 대가
그 제약을 Trivial 이라 부른다. 어떤 타입이 Trivial이라는 것은, 정리가 필요 없다는
것이다 —— 죽을 때 돌릴 것이 아무것도 없고, 그저 회수할 메모리만 있다. 정수, 정수의 튜플,
바이트를 다른 데서 빌린 문자열 슬라이스, 파스 트리의 노드 —— 모두 Trivial. region에
살 수 있는 것은 Trivial한 타입뿐이다.
이것은 설계가 사과하는 제한이 아니다. region이 값싼 것의 이유 그 자체다. 일괄 해제가 옳은 것은, 해제할 것이 바이트뿐일 때다. Trivial은 “영역을 한 수로 버린다”를 건전하게 만드는 성질을, 기억해 두어야 할 관례가 아니라 타입 시스템이 검사할 수 있는 규칙으로 적은 것이다. 이것은 언어가 곳곳에서 하는 같은 수다 —— 어떤 최적화가 유효하기 위해 성립해야 할 조건을, 당신의 규율 속에 암묵으로 두는 대신 타입에 명시한다.
Trivial의 반대는 Drop이다 —— 돌릴 정리를 가진 타입. 그리고 능력(capability) —— 바깥
세계에 손댈 권리를 나르는 값, 뒤 회의 주제 —— 은 대개 Drop이다. 긴장을 한 줄로 말하면
이렇다: region은 Trivial을 원하고, 관리할 가치가 있는 자원은 Drop이다. Drop한 값은
어디로 가는가?
결정: 메모리는 region, 정리는 with
출구는 셋이었고, 볼 가치가 있다. 이 선택은 결국, 언어가 몇 개의 개념을 짊어지는가에 대한 선택이기 때문이다.
한 안은, 역시 region에 Drop한 값을 담게 하고, region이 무너질 때 각 값의 정리를 역순으로
부르는 것이었다. 모든 것을 하나의 구문으로 접을 수 있다 —— 배울 두 번째 장치가 없다 ——
하지만 region을 값싸게 하던 성질도 부순다. region은 이제 돌릴 정리의 목록을 짊어지고, 그
파기는 더 이상 그저 포인터 리셋이 아니며, Trivial 최적화는 사라진다. 정리 순서도 “할당의
역순”으로 고정되는데, 그것이 의존 관계가 요구하는 순서라는 보장은 없다.
또 한 안은, 두 종류의 영역을 불러오는 것 —— Trivial 전용의 빠른 것과 Drop을 허용하는
범용의 것 —— 이지만, 그것은 바로 지난 회가 통째로 한 편을 들여 없애자고 논한 분할이다.
이걸 풀기 위해 불러오면, 얻어낸 것을 거래로 되돌려 주게 된다.
Mere는 셋째 길을 택했다: region은 엄격히 Trivial로 유지하고, 정리에는 전용 구문
with를 준다. 정리가 필요한 값은 with로 bind하고, 그 정리는 with 스코프가 끝날 때
돌린다.
with db = Database.connect("..."),
logger = Logger.buffered_stdout()
in {
region req_r {
// 요청 단위의 Trivial 데이터를 req_r에 할당한다
handle_request(req, db, logger, &req_r)
}
// req_r은 여기서 해제: 포인터 한 번의 리셋, 정리 없음 —— 전부 Trivial이었다
}
// with 스코프 종료: logger를 flush, 이어서 db를 close
이것이 사는 것은 키워드의 적음이 아니다 —— 각자 한 문장으로 말할 책임을 가진 두
장치다. region은 메모리를 관리한다: 바이트가 어디에 살고, 언제 영역이 회수되는가.
with는 정리를 관리한다: 어느 자원이 살아 있고, 언제 해제되는가. 코드를 보는 사람은 한
줄로, 각 구문이 무엇에 책임을 지는지 말할 수 있고, 둘은 결코 서로 번지지 않는다. 그
읽기 쉬움은, 언어가 곳곳에서 최적화하는 것과 같다. 여기서 그것은, 마침 둘 다 “스코프의
끝”에 발화하는 두 책임을 병합하지 않음으로써 사들여진다.
정리의 순서와, 끝에 닿지 않는 경로
정리는 LIFO 순으로 돌린다 —— with의 binding의 역순이다. 예에서 logger가 db보다
먼저 해제된다. logger가 두 번째로 bind되었기 때문이다. 이것은 C++의 RAII, Rust의
Drop과 같은 규칙이고, 옳은 기본값이다: 쌓은 순서의 역으로 무너뜨리므로, 어떤 자원이,
그 뒤에 구축된 무언가에 아직 쓰이고 있을지도 모르는 사이에 해제되는 일이 없다.
해피 패스보다 더 중요한 것은, 제어가 블록의 끝에 닿지 않을 때 무슨 일이 일어나는가다.
정상 출구에서만 성립하는 정리 보장은, 보장이 아니다. 그래서 with는, 스코프를 나가는
모든 경로에서 그 값을 해제한다:
with file = File.open("a.txt"),
db = Database.connect("...")
in {
if some_condition {
return error_case // 나가는 길에 db를 close, 이어서 file을 close
}
process(file, db)
// 정상 출구: db를 close, 이어서 file을 close
}
이른 return, 정상적인 빠져나감, 프레임을 되감는 panic —— 어느 것이나 같은 LIFO의 정리를
돌린다. 값은 스코프에서의 한 출구가 아니라, 스코프에 묶인다.
정리가 기본값이 아니라 결정일 때
닫는 법이 하나가 아닌 자원도 있다. 데이터베이스 트랜잭션은 commit도 rollback도 할 수 있고, 어느 쪽이 일어났는가는, 런타임이 골라야 할 기본값이 아니라 프로그램이 내리는 결정이다. Mere는 값을 소비할 수 있게 하여 이것을 다룬다:
with tx = db.begin() in {
if ok {
tx.commit() // tx를 소비(owned move) —— 그 Drop은 돌지 않는다
}
// 한 번도 commit되지 않은 tx는, 그 Drop으로 rollback된다
}
commit은 tx를 owned move로 취하므로, 그 뒤 tx는 사라지고, 스코프가 정리할 것은
아무것도 남지 않는다. commit되지 않고 끝으로 떨어진 tx는, 그래도 정리가 돌고 —— 그
정리는 rollback이다. 안전한 결말이 기본값이고, 다른 쪽 결말은 당신이 말해야 하는
무언가다. 메모리 모델의 소유권 규칙과, 여기서의 정리 규칙은, 같은 장치를 두 쪽에서 본
것이다.
이것이 정리한 것
Trivial과 with가 갖춰져, region 모델은 마침내 추론할 수 있을 만큼 완전해졌다. region은
Trivial한 데이터를 담고, 그것을 공짜로 회수한다. 진짜 정리를 가진 것은 with가 담고,
모든 출구 경로에서 LIFO 순으로 해제한다. 그리고 소유권 시스템은, 값을 소비함으로써 자동
정리에서 빼는 것을 허용한다. 두 구문, 각자 하나의 책임, 그리고 이름 있는 제약 ——
Trivial —— 이, 둘의 경계가 어디에 떨어지는가를 꼭 짚어 준다.
앞선 두 회가 미뤄 온 물음이 아직 하나 남아 있다: region 으로의 참조를 가진다는 것은 무엇을 뜻하며, 언어는 그런 참조가, 가리키는 region보다 오래 사는 것을 어떻게 막는가? 그것은 독립된 작은 이론이 된다. 다음 회: 뷰 타입, 그리고 region으로의 대여를 안전하게 만드는 세 규칙.