참조 카운팅 데이터를 더 인체공학적으로 다루기 위한 두 가지 기반 개선(Share 트레이트와 move 표현식)을 설계·프로토타이핑한다.
| 메타데이터 | |
|---|---|
| 담당 창구(Point of contact) | Niko Matsakis |
| 상태 | 제안됨(Proposed) |
| 트래킹 이슈 | rust-lang/rust-project-goals#107 |
| Zulip 채널 | 해당 없음(N/A) |
| lang 챔피언 | Niko Matsakis |
| 팀 | compiler, lang, lang-docs, libs |
| 작업 소유자(Task owners) | (없음) |
인체공학적인 참조 카운팅(ref-counting)을 위한 두 가지 기반 개선을 구현하고 프로토타입을 만든다: (1) clone이 동일한 기저 값(underlying value)에 대한 별칭(alias)을 만들어냄을 의미론적으로 식별하는 Share 트레이트, (2) 클로저가 무엇을 언제 캡처하는지 정밀하게 제어할 수 있게 해주는 move 표현식(move($expr)). 이 변경들은 향후 인체공학 개선을 위한 토대를 마련하는 동시에 즉각적인 가치를 제공하며, 프로토타입 목표 시점은 2026년 여름이다.
Rust에서 참조 카운팅 데이터를 다루는 것은 널리 알려진 고통 지점이다. 이 문제는 고수준 GUI 애플리케이션(Dioxus, Sycamore), 비동기 네트워크 서비스(tokio), 언어 상호운용(PyO3), 심지어 Rust 컴파일러 자체에도 영향을 미친다. 프로젝트들은 아레나(arena) 기반 설계부터 커스텀 전처리기까지, 이를 우회하기 위해 상당한 노력을 기울여 왔다.
이 목표는 광범위한 설계 탐구의 결실을 반영한다. RFC #3680이 .use 문법을 제안하고 엇갈린 피드백을 받은 뒤, 2025H2 동안 일련의 블로그 글과 커뮤니티 논의를 통해 설계 공간을 깊이 탐색했다. 그 논의는 목표를 재정의하는 데로 이어졌고, “커널에 쓸 만큼 저수준이면서, GUI에 쓸 만큼 사용하기 쉬운(low-level enough for a kernel, usable enough for a GUI)” 해법에 먼저 집중하게 되었다.
우리는 다음 두 기능을 추진할 것을 제안한다.
clone이 동일한 기저 값에 대한 별칭을 만들어내는 타입을 의미론적으로 식별하는 Share 트레이트;move($expr)).이 두 기능은 참조 카운팅 데이터를 더 인체공학적으로 다루게 해주면서도, Rust의 전통적 보장인 “모든 참조 카운트 변화는 코드에 드러난다(visible)”를 유지한다. 이 두 기능을 합친 것이 향후 참조 카운팅 데이터에 대한 다른 개선이 없음을 뜻하는 것은 아니다. 다만 이 기능들이 확실한 전진(step forward)이므로 우선 이를 취하고, 이후 추가로 무엇이 필요한지 평가하자는 취지다.
커널에 쓸 만큼 저수준이면서, GUI에 쓸 만큼 사용하기 쉬운. 저수준 세부를 필요로 하는 이들에게는 그 세부가 드러나야 하며, 동시에 고수준 애플리케이션에도 충분히 인체공학적이어야 한다.
동작(operational)보다 의미(semantic). 트레이트는 “비용이 무엇이냐”가 아니라 “무엇을 뜻하느냐”로 정의해야 한다. Share는 “별칭을 만든다”를 뜻해야지, “클론이 싸다”를 뜻하면 안 된다.
clone이 동일한 기저 값에 대한 별칭을 만들어내는 타입을 식별하는 새 트레이트:
rust#![allow(unused)] fn main() { trait Share: Clone { fn share(&self) -> Self { self.clone() } } impl<T: ?Sized> Share for Arc<T> {} impl<T: ?Sized> Share for Rc<T> {} impl<T: ?Sized> Share for &T {} impl<T> Share for mpsc::Sender<T> {} }
share() 메서드는 의미론적으로 clone()과 동등하지만, 이것이 독립 복사본이 아니라 별칭을 만든다는 점을 독자에게 신호한다. Arc, Rc, 채널 sender, 공유 참조 같은 타입이 Share를 구현할 것이다. Vec나 String 같은 타입은 구현하지 않는다—그 타입들의 클론은 독립적인 값을 만들어내기 때문이다.
이는 오늘날 실제로 존재하는 혼란을 해결한다. 예컨대 map.clone()을 보면, “같은 맵에 대한 두 번째 핸들(handle)”을 만드는지, “깊은 복사(deep copy)”를 만드는지 알기 어렵다. Share가 있으면 전자의 경우 map.share()라고 쓰게 되어 코드의 의도가 명확해진다.
클로저와 async 블록 내부에서 move($expr)는 클로저 생성 시점에 표현식을 평가하고 그 결과를 캡처한다:
rust#![allow(unused)] fn main() { // 현재: 임시 변수 때문에 어색함 let tx_clone = tx.clone(); tokio::spawn(async move { send_data(tx_clone).await; }); // move 표현식 사용: 인라인으로 명확함 tokio::spawn(async move { send_data(move(tx.clone())).await; }); }
이는 Rust의 기존 클로저 모델을 일반화한다. “ref 클로저”와 “move 클로저”를 따로 두는 대신, 일부 캡처가 move()를 사용하는 클로저를 갖게 된다. move || 문법은 “모든 곳에 move()를 사용하라”는 약어(shorthand)가 된다.
Share와 결합하면:
rust#![allow(unused)] fn main() { tokio::spawn(async move { do_something(move(self.some_a.share()), move(self.some_b.share())); }); }
| 작업 | 담당자 | 비고 |
|---|---|---|
Share 트레이트 RFC | Niko Matsakis | 의미론 정의, 표준 라이브러리 구현 포함 |
| move 표현식 RFC | Niko Matsakis | 클로저 디슈가링(Desugaring) 의미론 |
Share 트레이트 구현 | Santiago Pastorino | |
| move 표현식 구현 | Santiago Pastorino | |
| 레퍼런스 변경 준비 | Niko Matsakis | |
Share 트레이트 안정화 PR 준비 | Santiago Pastorino | |
move 표현식 안정화 PR 준비 | Santiago Pastorino |
목표: 2026년 여름까지 nightly에서 동작하는 프로토타입.
이 섹션은 Rust 팀들로부터 어떤 지원이 필요한지 개요를 제시한다. 각 팀마다 필요한 지원 수준을 식별하라:
- Vibes: 팀이 실제로 뭔가를 해줄 필요는 없지만, 아이디어를 좋아하는지 알고 싶다.
- 예: crates.io에서 새 기능을 프로토타이핑하고, 나중에 업스트림하길 희망하는 경우.
- 예: 나중에 언어 기능이 될 수 있는 연구를 수행하는 경우.
- Small: 팀이 통상적인 활동만 하면 된다.
- 예: 몇 개의 작은 PR 리뷰가 필요한 컴파일러 변경.
- 예: lang 팀에 린트 승인 요청.
- Medium: 한 사람의 전담 지원이 필요하지만, 팀 전체가 많은 일을 하진 않아도 된다.
- 예: 재설계(rearchitecting)까지는 필요 없는 컴파일러 변경.
- 예: 작고 논쟁의 여지가 적은 언어 기능 구현.
- Large: 팀 전체의 깊은 리뷰가 필요하다.
- 예: 컴파일러 일부를 재설계.
- 예: 설계 미팅이 필요한 복잡한 언어 기능 구현.
확신이 없다면 비워 두어도 된다. 프로젝트 목표 팀이 도울 수 있다.
“Vibes”와 “Small” 수준 요청은 팀 내 누군가가 목표에 “세컨드(second)”를 해주면 되고, “Medium”과 “Large” 수준 요청은 팀의 전담 챔피언이 필요하다. 세컨드나 챔피언이 없다면 프로젝트 목표 팀이 찾아줄 것이니 걱정하지 말라.
이 목표는 긴 여정을 거쳤다:
2024H2: Dioxus의 Jonathan Kelley가 고수준 Rust에 관한 블로그 글을 작성했고, 이것이 프로젝트 목표로서 인체공학적 참조 카운팅 노력의 계기가 되었다.
2025H1: RFC #3680이 .use 문법과 use || 클로저를 제안했다. Santiago Pastorino가 nightly에서 실험적 지원을 구현했다. 커뮤니티 피드백은 문제를 다루는 데 대해서는 긍정적이었지만, “필수 문법을 더 추가하는 것이 정말로 인체공학을 개선하느냐”에 대한 우려를 제기했다.
2025H2: 설계 미팅, RustConf Unconf, 그리고 광범위한 블로깅을 통해 설계 공간을 더 깊게 탐구했다. 그 과정에서 다음과 같은 핵심 통찰이 나왔다:
동작보다 의미: “무엇이 싸게 클론되는가”(동작적/operational)로 트레이트를 정의하기보다는, “클론이 무엇을 뜻하는가”(의미론적/semantic)에 초점을 맞춰야 한다. Arc를 클론하면 같은 값에 대한 두 번째 _핸들_을 얻게 되는데, 이 “얽힘(entanglement)”이 핵심 속성이다.
명시는 인체공학적일 수 있다: Josh Triplett과의 대화 이후, 일부 애플리케이션은 실제로 별칭이 생성되는 지점을 추적할 필요가 있다는 결론에 도달했다. 목표는 모든 것을 암묵적으로 만드는 것이 아니라, 명시적인 코드를 인체공학적으로 만드는 것이어야 한다.
Move 표현식은 깔끔하게 일반화된다: move($expr) 문법은 별도의 병렬 시스템을 추가하기보다 Rust의 기존 클로저 모델을 우아하게 확장한다.
Dioxus 블로그 글의 “Cloudflare 예시”는 현재의 고통을 잘 보여준다:
rust#![allow(unused)] fn main() { let _some_a = self.some_a.clone(); let _some_b = self.some_b.clone(); let _some_c = self.some_c.clone(); tokio::task::spawn(async move { do_something(_some_a, _some_b, _some_c) }); }
Share와 move 표현식을 사용하면:
rust#![allow(unused)] fn main() { tokio::task::spawn(async { do_something( move(self.some_a.share()), move(self.some_b.share()), move(self.some_c.share()), ) }); }
이는 더 간결하고, 클로닝이 여전히 눈에 보이며, 어색한 임시 변수를 제거한다.
우리는 여러 이름을 고려했다. Handle은 좋은 명사지만 동사로 쓰기엔 어색하다(“이 값을 handle 하라?”). Alias는 명사와 동사 모두 가능하지만 다소 기술적으로 느껴진다. Share는 흔하고 직관적이며, Rust에서도 이미 쓰이는 단어다(&T는 “shared reference”). share() 메서드는 자연스럽게 읽힌다: “이 값을 클로저와 공유하라.”
일부 애플리케이션은 성능 디버깅, 메모리 누수 조사, 또는 Arc::make_mut 같은 API 때문에, 별칭이 생성되는 지점을 실제로 추적해야 한다. 모든 것을 자동으로 만들면 고수준 앱에는 잘 맞지만, “커널에 쓸 만큼 저수준” 테스트를 통과하지 못한다. 우리의 접근은 별칭을 눈에 보이게 유지하면서 보일러플레이트를 줄인다. 이 작업이 끝나면, 추가 변경을 계속할지 평가할 것이다.
RFC #3680은 .use 문법과 Use 트레이트를 제안했다. 그 작업은 여전히 nightly에서 사용 가능하며 우리의 사고에 영향을 주었다. 이 목표는 커뮤니티 피드백과 더 깊은 탐구에 기반한, 정제된 방향을 나타낸다. Share 트레이트는 정신적으로는 Use와 비슷하지만 의미론이 더 명확하다. move 표현식은 use || 클로저가 다루려던 문제와 유사한 문제를 해결하지만, 더 자연스럽게 일반화된다.
rust-lang/rust-project-goals#107: https://github.com/rust-lang/rust-project-goals/issues/107