rust관련 블로그를 찾아 보면서 흥미로운 글을 보게 되었습니다.
Return type notation
일명 RTN이라고 불리어집니다.
이 개념은 아직 rust에 적용된 개념은 아니지만 충분히 고려가 필요한 개념이라고 생각합니다.
아래와 같은 코드가 있다고 가정을 해봅시다.
trait HealthCheck {
async fn check(&mut self, server: &Server) -> bool;
}
위의 푶현은 async_fn_in_trait
를 활성화해서 표현된 코드입니다.
(async_fn_in_trait는 여기 참고하면 됩니다.)
이를 다음 코드에서 실행을 하면
fn start_health_check<H>(health_check: H, server: Server)
where
H: HealthCheck + Send + 'static,
{
tokio::spawn(async move {
while health_check.check(&server).await {
tokio::time::sleep(Duration::from_secs(1)).await;
}
emit_failure_log(&server).await;
});
}
다음과 같은 오류가 발생을 합니다.
note: future is not `Send` as it awaits another future which is not `Send`
--> src/lib.rs:16:15
|
16 | while health_check.check(&server).await {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ await occurs here
이와 같은 오류가 발생한 이유는 tokio::spawn
함수 내의 타입은 Send
가 구현되어야 하는 것인데 health_check.check
에서 반환되는 타입은 Send
가 구현되지 않았기 때문입니다.
이는 HealthCheck
trait를 desugaring하면 원인을 알 수 있습니다.
trait HealthCheck {
// async fn check(&mut self, server: &Server) -> bool;
fn check(&mut self, server: &Server) -> impl Future<Output = bool>;
// ^ Problem is here! This returns a future, but not necessarily a `Send` future.
}
보시는 거와 같이 check함수의 반환 타입은 Furture
trait만 구현될 뿐 Send
까지 return되는 게 아니었습니다.
이 문제는 단순하게 Send
같이 구현되도록 하면 해결이 되지만 모든 Future
에 Send
까지 넣을 필요는 없습니다. 왜냐하면 어떤 Future
는 thread사이에 공유하면 안되는 케이스가 존재하기 때문입니다.
블로그 저자는 이를 단순하게 Trait 구현으로 문제를 해결 하는 것 보다는 Trait의 함수 반환 타입에 대한 제약을 주어야 하는 생각을 하게 되었습니다.
그래서 저자가 생각한 표현은 다음과 같습니다.
fn start_health_check<H>(health_check: H, server: Server)
where
H: HealthCheck + Send + 'static,
H::check(..): Send, // <— return type notation
H
타입에서 check
함수의 반환 타입은 Send
가 구현되어 있어야 한다는 뜻입니다.
여기서 의문이 드는 것은 그냥 associated Type 만들어서 거기에다가 Send
가 구현되어 있는지 확인하면 되지 않을까라는 생각입니다.
에를 들어,
trait HealthCheck {
// async fn check(&mut self, server: Server);
type Check<‘t>: Future<Output = ()> + ‘t;
fn check<‘s>(&’s mut self, server: Server) -> Self::Check<‘s>;
}
이를 함수 부르는 쪽에서 처리할 때는,
fn start_health_check<H>(health_check: H, server: Server)
where
H: HealthCheck + Send + 'static,
for<‘a> H::Check<‘a>: Send, // <— equivalent to `H::check(..): Send`
이렇게 처리할 때는 몇가지 문제점이 있습니다.
higher-ranked trait bounds
를 써야 한다.이러한 문제점들이 있기 때문에 되도록이면 associated type와 상관없이 where clause내에 표현할려는 것입니다.
그리고 몇가지 이점이 있다.
첫번째 사항은 다음과 같습니다.
fn get() -> impl FnOnce() -> u32 {
move || 22
}
fn foo() {
let c: get() = get();
let d: u32 = c();
}
즉, get자체가 타입 처럼 표현이 가능해집니다.(이건 실험적인 거여서 언제든지 바뀔 수 있습니다.)
이를 통해 trait내에서 무의미하게 associated type을 정의해서 제약을 걸지 않고 보다 여러 trait의 함수의 반환 타입에 대응이 가능해집니다. 그리고 함수의 반환타입을 이름 짓는 게 가능해집니다.
굉장히 공감이 되고 필욮한 기능이라고 생각이 든다. 그래서 보다 효과적으로 함수 반환 타입을 사용할 수 있지 않을까 싶습니다.
(나중에 한번 제가 작업을 진행할까 생각중...)
https://smallcultfollowing.com/babysteps/blog/2023/02/13/return-type-notation-send-bounds-part-2/
재밌는 글 잘 읽었습니다.