Rust의 효과, 부분구조적 타입, 정제 타입을 중심으로 언어가 나아갈 수 있는 발전 방향을 정리한다.
Rust를 위한 거대한 비전
Rust와 그 기능들에 대해 꽤 많이 글을 써 왔는데도, 내가 Rust에 대해 품고 있는 “거대한 비전”을 지금까지 제대로 말로 정리해 본 적은 없는 것 같다. 여기서 할 말은 많지만, 현재 내가 특히 흥미롭게 보는 발전 방향은 세 가지다:
언어 안에는 기대할 만한 것들이 더 많지만, 특히 이 세 가지가 내 관심을 끈다. 지난 몇 년간 내가 Rust에서 한 작업을 조금이라도 따라왔다면, 내가 여기서 정확히 무엇을 향해 가고 있는지에 대한 윤곽이 좀 더 분명해질지도 모른다.
Rust는 안정 채널에서 const fn과 async fn을 지원한다. 그리고 nightly에서는 try fn과 gen fn도 지원한다. 이런 것들은 흔히 “함수 색(function colors)”이라고 불리지만, 더 엄밀하게는 “효과 타입(effect types)”이라고도 하며, “type-and-effect systems”라는 더 넓은 프로그래밍 언어 연구 범주에 속한다.
효과가 한두 개뿐이라면 대체로 괜찮다. 하지만 효과를 더 많이 추가할수록 다루기가 더 고통스러워진다. 그리고 컴파일러, 운영체제, VM 같은 것을 만드는 사람들과 대화를 나눠 본 결과, Rust는 다음을 포함해 함수에 대해 더 많은 보장을 제공할 수 있다면 큰 이득을 볼 것이라고 나는 믿는다:
panic 효과의 부재)div 효과의 부재)ndet 효과의 부재)io 효과의 부재)도입해야 할 함수의 종류가 정말 많다. 하지만 Rust가 쓰이길 원하는 종류의 시스템에서는 이 모든 것이 믿을 수 없을 정도로 유용하다. 그래서 나는, 이런 종류의 함수를 사용감이 좋은 방식으로 도입할 수 있게 해 주는 올바른 추상화를 추가하는 데 관심이 있다.
Rust의 가장 큰 특징은 빌림 검사기(borrow checker)의 도입이다. 이는 런타임에 가비지 컬렉터가 없어도 메모리 안전성을 정적으로 보장한다. 형식적으로 말하면 Rust의 타입 시스템은 아핀(affine) 으로 간주되는데, 이는 각 값이 최대 한 번 사용되어야 함을 의미한다. 그리고 “최대 한 번 사용”은 “use after free” 같은 버그가 없음을 보장하는 데 정확히 필요한 조건이다.
하지만 더 많은 보장을 제공할 수 있는 타입 시스템도 있다. “최대 한 번 사용”에서 한 단계 더 나아가면 “정확히 한 번 사용”이다. 이런 보장을 제공하는 타입은 “선형(linear)”이라고 불리며, “use after free”의 부재뿐 아니라 메모리 누수의 부재도 보장할 수 있다.
선형 타입에서 한 단계 더 나아가면 “순서(ordered) 타입”이다. 이 타입들은 정확히 한 번 사용될 뿐 아니라, 도입된 정확한 순서대로 정확히 한 번 사용된다. 이것이 실제로 의미하는 바는 이렇다: 이들은 안정적인 메모리 주소를 가지며, 드롭될 때까지 그 주소가 절대 바뀌지 않는 타입이다. 나란히 정리하면 다음과 같다:
| Type | Usage | Guarantees |
|---|---|---|
| Affine types | 최대 한 번 | “use after free”가 더 이상 없음 |
| Linear types | 정확히 한 번 | 메모리 누수가 더 이상 없음 |
| Ordered types | 정확히 한 번, 순서대로 | 안정적인 메모리 위치 |
여기서 “contraction”이나 “weakening” 같은 용어로 지루하게 하지는 않겠다. 더 알고 싶다면 위키피디아 페이지를 읽어 보라. 하지만 Rust에 한정해서 말하면: 우리가 Move와 Forget 같은 새로운 트레이트를 작업해 온 이유가 바로 이것이다:
!Forget은 선형 타입을 연다!Move는 순서 타입을 연다 1“use after free”는 더 형식적으로는 시간적 메모리 안전 위반(temporal memory safety violation) 으로 알려져 있다. 그 짝은 “out of bounds error”로, 이는 공간적 메모리 안전 위반(spatial memory safety violation) 이라고도 한다(ref). Rust에서는 빌림 검사기가 정적으로 “use after free” 버그가 절대 발생하지 않음을 보장할 수 있다. 하지만 그 규칙을 조금 완화하고 싶다면 Rc나 Arc 같은 타입을 선택해 런타임에 그 속성들을 검사하도록 옵트인할 수 있다.
하지만 경계 검사(out of bounds checks)에 관해서는 상황이 조금 다르다. 기본값은 대개 런타임에 경계를 검사하는 것이고, 컴파일러가 때때로 이를 생략할 수도 있지만 그것은 최적화로서만 가능하다. 즉 우리는 메모리 안전을 위해 런타임 성능을 일부 포기하고 있는 셈이다.
그런데 런타임 대신 컴파일 시간을 메모리 안전과 맞바꿀 수 있다면 어떨까? 기존 타입에 추가적인 보장을 붙일 수 있는 타입 시스템을 정제 타입 시스템(refinement type systems) 이라고 부른다. 그리고 Rust에서는 이를 극도로 가볍게 만든 버전을 실험 중인데, 우리는 이를 패턴 타입(pattern types) 이라고 부르고 있다.
패턴 타입은 Rust의 패턴 문법을 사용해 기존 타입에 주석을 달 수 있게 한다. 예를 들어 NonZeroUsize를 보자: 이는 레이아웃에서 니치(niche)를 활용할 수 있도록 오랫동안 수많은 커스텀 컴파일러 최적화에 의해 뒷받침되어 왔다. 하지만 패턴 타입을 사용하면, 패턴으로 usize 타입을 정제(refine) 함으로써 같은 최적화를 자동으로 얻을 수 있다:
type NonZeroUsize = usize is 1..;
// ^^^^^^ 패턴을 사용한 정제
패턴 타입과 밀접하게 관련된 기능으로 뷰 타입(view types) 이 있는데, 이는 컴파일러가 별칭(aliasing)을 추론할 때 서로 분리된(disjoint) 패턴을 고려할 수 있게 해 준다(ref). 이는 각 참조가 다른 필드를 전혀 보지 않는 한, 같은 타입에 대한 두 개의 가변 참조를 들고 있을 수 있게 해 준다는 점에서 훌륭하다.
나는 패턴 타입과 뷰 타입을 Rust의 빌림 검사기 이야기를 더욱 더 좋게 만들 수 있는 방법으로 본다. 런타임 검사 vs 메모리 안전이라는 근본적 트레이드오프를 제거하는 것(패턴 타입)과, 더 많은 유효한 빌림을 표현 가능하게 만드는 것(뷰 타입), 두 측면 모두에서 말이다.
Rust에는 기대할 만한 것들이 정말 많다. 나는 언어의 형식성을 개선하는 작업, 컴파일러 개선, 생태계에서 일어나는 개선을 사랑한다. 그리고 내가 언급하지는 않았지만 진행 중인 언어 개선도 더 많고, 그 또한 기대된다(보고 있다, reflection).
하지만 개인적으로 나는? Rust가 존재하는 생산용 언어 중 가장 안전한, 정말 끝내주는 언어가 되길 바란다. 우리가 나쁘지 않긴 하지만, 아직 Ada/SPARK는 아니다. 그리고 이런 기능들을 작업하는 것은 내가 흥미롭고 설레는 일이다. 내가 Rust에 자원해서 참여하는 이유이기도 하다.
Rust를 근본적으로 개선하는 방법을 찾아내는 일은 쉽지도 빠르지도 않다. 하지만 결국에는 분명 가능하고, 해볼 가치가 있으며, 내 생각엔: 정말 재미있다!
Pin과 달리, 언어의 나머지 부분과 실제로 합성(compose)되는 방식으로 순서 타입을 연다. 완전히 인체공학적이기 위해서는 언어 차원의 emplacement에도 의존하는데, 나는 이것이 효과로 표현되는 것이 가장 좋다고 믿는다.←모든 참고문헌 보기