컨텍스트와 capability의 관점에서, 트레이트 바운드가 런타임 값을 함께 운반한다면 Rust의 딕셔너리 전달 스타일과 소유권, 메서드, 스코프별 impl이 어떻게 달라지는지 탐구합니다.
Nadri's musings- [x]
Mar 22, 2026
제 지난 글에서는 트레이트가 함수 사이로 메서드 묶음을 전달하는 것처럼 동작하지만, 컴파일러가 이를 자동으로 추론해 준다는 점을 보여드렸습니다.
Tyler Mandry는 이것이 contexts/capabilities와 똑같아 보인다고 재빨리 지적했습니다.
이번 글에서는 그 밑바탕에 있는 질문을 살펴보겠습니다. 트레이트 바운드도 값을 함께 담는다면 어떨까요?
먼저 Rust 기능 아이디어 하나로 시작하겠습니다. 저는 이것을 처음 접한 뒤로 아주 들떠 있었고, Tyler가 자신의 블로그 글에서 훌륭하게 설명했습니다. 직접 읽어보시길 권하지만, 여기서는 핵심만 요약하겠습니다.
이 기능은 세 가지 요소로 이루어집니다.
capability my_capability;
;
2. 이제 plaintext my_capability: Type 를 ```plaintext
where
3. 특정 스코프에 대한 값은 다음과 같이 제공됩니다.
```plaintext
with my_capability = some_value() { ... }
.
이것은 특히 트레이트 impl에서 아주 멋집니다.
capability arena;
impl<'a> Deserialize for &'a Foo
where
arena: &'a BasicArena,
{
...
}
fn main() -> Result<(), Error> {
let bytes = read_some_bytes()?;
with arena = &arena::BasicArena::new() {
let foos: Vec<&Foo> = deserialize(bytes)?;
println!("foos: {:?}", foos);
}
Ok(())
}
여기서 일어나는 일은 plaintext <&Foo as Deserialize> 에 대한 dictionary1가 이제 런타임 값도 함께 담는다는 것입니다. 컴파일러는 ```plaintext
T: Deserialize
## 이제 dictionary가 값을 담는다
아무것도 모르는 함수들 사이로 값을 암묵적으로 전달하면, 당연히 몇 가지가 달라집니다.
### 암묵적 값 제어하기
Rust의 다른 제네릭과 마찬가지로, 어떤 값을 지원할 수 있는지 어느 정도 제어할 방법이 필요합니다. Tyler는 다음과 같이 제안합니다.
fn deserialize_and_print_later<T>(deserializer: &mut Deserializer) where with('static + Send) T: Deserialize { ... }
우리의 dictionary 관점에서는 이렇게 써도 무방합니다.
fn deserialize_and_print_later<T>(deserializer: &mut Deserializer) where T: Deserialize, <T as Deserialize>: 'static + Send, { ... }
여기서 ```plaintext
<T as Deserialize>
``` 는 dictionary 자체를 가리키는 것으로 이해하므로, 다른 타입과 마찬가지로 여기에 바운드를 적용할 수 있습니다[2](https://nadrieril.github.io/blog/2026/03/22/what-if-traits-carried-values.html#fn:1).
### 선형성
이 아이디어를 진지하게 받아들였을 때 아마 가장 미친 결과는, 이제 트레이트 바운드가 소유권 의미론을 가져야 한다는 점일 것입니다. 다음을 생각해 봅시다.
impl<'a> Deserialize for &'a Foo where arena: &'a mut Vec<Foo>, { ... }
이제 dictionary는 ```plaintext
&mut
``` 를 담고 있으니, 동시에 두 함수에 넘기지 않도록 조심해야 합니다! 다음과 같은 함수는 우리의 ```plaintext
&Foo
``` 에 대해 동작할 수 없습니다.
fn bar<T: Deserialize>(bytes: Vec<u8>) {
// The iterator needs to capture the &mut context.
for item in whatever(bytes).map(|x| T::deserialize(x)) {
// Trying to use it here too is an aliasing violation.
let other_item = T::deserialize(something_else(bytes));
...
}
}
따라서 우리는 마주칠 수 있는 소유권 의미론의 네 가지 종류를 구분해야 합니다.
* 오늘날의 기본값, 즉 암묵적 값이 전혀 없는 경우: ```plaintext
<T: Trait>: const
3;
&Context
와 같은 의미론:plaintext
<T: Trait>: Copy
;
* ```plaintext
&mut Context
``` 와 같은 의미론: ```plaintext
<T: Trait>: Reborrow
``` ([the project goal](https://github.com/rust-lang/rust-project-goals/issues/399)의 ```plaintext
Reborrow
``` 트레이트 사용)[4](https://nadrieril.github.io/blog/2026/03/22/what-if-traits-carried-values.html#fn:4);
* ```plaintext
Box<Context>
``` 와 같은 의미론: ```plaintext
<T: Trait>
``` 는 무엇이든 담을 수 있음.
여기서 여러분은 4개의 클로저 트레이트 [```plaintext FnStatic ```](https://github.com/rust-lang/rust/issues/148768), ```plaintext
Fn
```, ```plaintext
FnMut
```, ```plaintext
FnOnce
``` 와의 유사성을 알아보셨을 겁니다.
아, 그리고 소유된 경우에는 트레이트 바운드가 사용되지 않으면 상당한 ```plaintext
Drop
``` 이 일어날 수도 있습니다 :3
이건 너무나도 파격적이라서, 저는 컨텍스트를 그냥 ```plaintext
Copy
``` 로 제한하고 interior mutability를 쓰자고 제안하고 싶습니다. 하지만 완전한 표현력을 갖추면 정말 맛있는 API가 나올 수도 있을 것 같기도 합니다 👀. 댓글로 꼭 공유해 주세요. 보고 싶습니다.
### 메서드는 이제 클로저다
클로저 트레이트 이야기가 나왔으니 말인데, 이제 메서드도 클로저입니다. 위 예시에서 ```plaintext
<T as Deserialize>::deserialize
``` 는 암묵적 매개변수를 사용하므로, 이를 ```plaintext
fn(D) -> Result<.., ..>
``` 함수 포인터로 캐스팅할 수 없습니다.
```plaintext
<T as Deserialize>
``` 의 소유권 의미론에 따라, 그 메서드들은 그에 대응하는 ```plaintext
Fn*
``` 클로저 트레이트를 구현하게 됩니다.
### 스코프별 impl
Tyler가 자신의 블로그 글에서 지적했듯이, 이제 트레이트 바운드는 더 이상 전역적으로 참인 사실이라고 볼 수 없습니다! 어떤 capability가 스코프 안에 있는지에 따라, 같은 ```plaintext
MyType: Trait
``` 가 성립할 수도 있고 아닐 수도 있습니다.
이것은 놀랄 만큼 표현력이 큰 새로운 능력이며, 기능 집합을 약간만 더 확장하면 특히 그렇습니다.
struct MagicPointer<'a>(PhantomData<&'a ()>);
capability pointer_target;
// This Deref impl is only available when the capability is in scope,
// and it has a different target type depending on scope!
impl<'a> Deref for MagicPointer<'a>
where
pointer_target: impl Sized + 'a // Can be basically anything
{
type Target = type_of!(pointer_target); // I cheat, don't tell
fn deref(&self) -> &Self::Target {
&pointer_target
}
}
이것은 우리가 트레이트를 사용하는 방식에 광범위한 결과를 가져옵니다.
struct MyInt(u32);
capability salt;
// This impl is correct inside a given context. But switching contexts breaks it:
// two equal values may hash differently in different contexts.
impl Hash for MyInt
where
salt: u32
{
... // hash self.0.xor(salt)
}
fn main() { let mut set: HashSet<MyInt> = Default::default(); with salt = 42 { set.insert(0); } with salt = 10 { if set.contains(&0) { // completely not clear whether that's the case. // depends on impl details } } }
위의 ```plaintext
Hash
``` impl은 유효하지 않은 것으로 간주되거나, 아마 그럴 가능성이 큽니다. 또는 ```plaintext
HashSet
``` 같은 자료구조가 스코프별 impl을 opt-in하지 않을 수도 있습니다. 어느 쪽이든 이것은 표현력의 새로운 차원을 엽니다.
## impl 캡처하기
글의 도입부에서는 context/capability로 시작했지만, 트레이트 바운드가 값을 담게 만드는 다른 방법도 떠올릴 수 있습니다. 다른 하나는 impl이 자신의 컨텍스트로부터 캡처하게 하는 것입니다!
이를 위해 [```plaintext move ``` expressions](https://smallcultfollowing.com/babysteps/blog/2025/11/21/move-expressions) 아이디어를 다시 쓰겠습니다. 다만 저는 이것을 ```plaintext
capture
``` expressions라고 부르는 편이 더 좋습니다. 그리고 여전히 스코프별 impl이라는 개념도 필요하며, 저는 이를 ```plaintext
local impl
``` 이라고 쓰겠습니다.
struct Context;
trait GimmeArena { // The crazy lifetime syntax would mean "borrows from the trait dictionary". fn gimme() -> &'{Self as GimmeArena} Arena; }
fn use_arena() where Context: GimmeArena { let arena = Context::gimme(); // use the arena }
fn foo() { let arena = Arena;
local impl GimmeArena for Context {
fn gimme() -> &'{Self as GimmeArena} Arena {
capture(&arena)
}
}
// In this scope, `Context` implements `GimmeArena`, and the dictionary
// carries a reference to the arena.
use_arena();
}
fn bar() { // Different scope, so we can make another impl. local impl GimmeArena for Context { fn gimme() -> &'{Self as GimmeArena} Arena { ... // do something else } }
use_arena();
}
기능 측면에서 이것은 꽤 비슷합니다. ```plaintext
where Context: GimmeArena
``` 는 앞서의 ```plaintext
where arena: &'a Arena
``` 와 아주 가깝습니다.
이것은 또 다른 의미의 “capturing impl”과도 충돌합니다. 즉 캡처된 값들이 ```plaintext
&mut self
``` 인자를 통해 접근 가능해지는 경우인데, 그러면 ```plaintext
Iterator
```, visitor 등을 편리하게 정의할 수 있게 됩니다.
종합적으로 보면 저는 이 방향에 아주 확신이 서지는 않습니다. 그래도 어느 쪽이든 중요한 개념이 데이터를 담는 impl이라는 점을 잘 보여주기 때문에 소개했습니다.
## 결론
capability, 그리고/또는 트레이트-바운드-값 개념에 여러분도 흥미를 느끼셨기를 바랍니다! 제가 매력적으로 느끼는 점은 “트레이트 바운드가 런타임 값을 담는다”는 생각이 언어의 나머지 부분과 아주 자연스럽게 상호작용한다는 것입니다.
딕셔너리 전달 스타일 트레이트에 대한 더 많은 탐구로 나중에 다시 찾아뵙겠습니다.
1. 제 [지난 글](https://nadrieril.github.io/blog/2026/03/20/dictionary-passing-style.html)을 보세요.[↩](https://nadrieril.github.io/blog/2026/03/22/what-if-traits-carried-values.html#fnref:3)
2. 여기서는 타입과 값의 차이를 조금 얼버무리고 있지만, 실제로는 모호하지 않다고 생각합니다. 이를 더 정확하게 표현하는 방법은 마법 같은 연관 타입 ```plaintext
<T as Deserialize>::capabilities: 'static + Send
``` 를 두는 것입니다.[↩](https://nadrieril.github.io/blog/2026/03/22/what-if-traits-carried-values.html#fnref:1)
3. 기본적으로 “컴파일 타임에 완전히 알려져 있다”는 뜻이므로, 어떤 값을 전달할 필요가 없습니다. 이것은 ```plaintext
T: const Trait
``` 와는 아주 다릅니다. 후자는 “그 메서드를 컴파일 타임에 호출할 수 있다”는 뜻이니까요. 아마 이렇게 비슷한 표기법은 쓰지 않게 될 겁니다. 엄청 헷갈리니까요 x)[↩](https://nadrieril.github.io/blog/2026/03/22/what-if-traits-carried-values.html#fnref:2)
4. 제가 이 트레이트를 제대로 쓰고 있는지는 잘 모르겠지만, 제 이해로는 “```plaintext
Copy
``` 이긴 한데 새 값이 이전 값과 borrowck로 연결되어 있는 것”이며, 우리에게는 이런 종류의 무언가가 필요합니다.[↩](https://nadrieril.github.io/blog/2026/03/22/what-if-traits-carried-values.html#fnref:4)
[](https://nadrieril.github.io/blog/2026/03/22/what-if-traits-carried-values.html)
* [](https://nadrieril.github.io/blog/feed.xml "Subscribe to syndication feed")