Rust Return type notation

maxtnuk·2023년 2월 26일
0
post-thumbnail

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 같이 구현되도록 하면 해결이 되지만 모든 FutureSend까지 넣을 필요는 없습니다. 왜냐하면 어떤 Future는 thread사이에 공유하면 안되는 케이스가 존재하기 때문입니다.

블로그 저자는 이를 단순하게 Trait 구현으로 문제를 해결 하는 것 보다는 Trait의 함수 반환 타입에 대한 제약을 주어야 하는 생각을 하게 되었습니다.

RTN (Return Type Notation)

그래서 저자가 생각한 표현은 다음과 같습니다.

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`

이렇게 처리할 때는 몇가지 문제점이 있습니다.

  • trait내의 associated type를 불필요하게 노출하게 됩니다.
    • 함수를 정의하는 입장에서 각 trait마다의 associated type를 확인하고 구현해야 하는 번거로움이 생깁니다.
  • lifetime을 써야 하는 반환 타입일 경우 higher-ranked trait bounds를 써야 한다.
    • 번거롭게 여러 lifetime을 정의해야 하는 문제점이 생깁니다.

이러한 문제점들이 있기 때문에 되도록이면 associated type와 상관없이 where clause내에 표현할려는 것입니다.
그리고 몇가지 이점이 있다.

  • 함수 자체를 type 처럼 표현이 가능해진다.
  • 여러 trait 함수의 반환 타입을 여러 번 정의가 가능해집니다.

첫번째 사항은 다음과 같습니다.

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/

profile
Rust로 무난하게 개발하는 사람

0개의 댓글