👻

[Rust]Github GraphQL APIで学んだライフタイム

2024/04/05に公開

はじめに

Rustのライフタイムはなんかずっと分からないままだったんですが、Github GraphQL API使って遊んでたらちょっとだけ理解できたので実例として残しておきます。

設定

以下のようなGraphQLのクエリを投げました。割とネストが深めのクエリです。

query CommitQuery {
  repository(owner: "example", name: "example_repo") {
    ref(qualifiedName: "main") {
      target {
        __typename
        ... on Commit {
          history {
            edges {
              node {
                author {
                  name
                }
              }
            }
          }
        }
      }
    }
  }
}

edgesの下のnodeだけを受け取りたいので、extract_node_dataという関数を作り、以下のように呼び出しました。

let response_data = request::<_, commit_query::ResponseData>(&client, &body).await?;

let nodes = extract_node_data(response_data);

このextract_node_dataで色々試しました。

試したこと

nodesの実体を返す

最初に試したコードは以下です。

fn extract_node_data(
    response_data: commit_query::ResponseData,
) -> Option<Vec<CommitQueryRepositoryRefTargetOnCommitHistoryEdgesNode>> {
    let repository = response_data.repository?;
    let ref_ = repository.ref_?;
    // ...省略

    let mut nodes = Vec::new();
    commit.history.edges?.iter().for_each(|edge| {
        if let Some(edge) = edge {
            if let Some(node) = edge.node {
                // node: CommitQueryRepositoryRefTargetOnCommitHistoryEdgesNode
                nodes.push(node);
            }
        }
    });

    Some(nodes)
}

まずはこのコードを試すとedge.nodeで以下のエラーが出ました。

cannot move out of `edge.node` as enum variant `Some` which is behind a shared reference

これはedge.nodeedgeは参照型(&CommitQueryRepositoryRefTargetOnCommitHistoryEdges)になっているので、そこからデータを新しい値に割り当てることができない、つまり所有権を渡すことができないということなので、nodeも参照型として以下のように割り当てて、返り値も参照型にします。

fn extract_node_data(
    response_data: commit_query::ResponseData,
) -> Option<Vec<&CommitQueryRepositoryRefTargetOnCommitHistoryEdgesNode>>
// 省略
if let Some(node) = &edge.node {
                // node: &CommitQueryRepositoryRefTargetOnCommitHistoryEdgesNode
                nodes.push(node);
            }
Some(nodes)

そうすると返り値の&のところでエラーが出ます。

help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime

これは、nodesは参照型を返そうとしているけれども、その参照(借用)がどこからも来ていない、つまり有効なライフタイムを持つ値がないということです。
どこからも来ていないというのはおそらく本来は借用元がこの関数を呼ぶ側で明示されているはずということだと思います。関数内で参照してても関数が終わると参照元も無くなってしまうので、コンパイラのアドバイス通りライフタイムを明示します。

fn extract_node_data(
    response_data: commit_query::ResponseData,
) -> Option<Vec<&'static CommitQueryRepositoryRefTargetOnCommitHistoryEdgesNode>>

これでプロセスが生きてる間はライフタイムが生き残る値を返すことになりますが、そもそも関数自体は実体を受け取ったのに、関数内でiter()を使ったために参照型に変換されたものを参照型で返すのって変な気がしますね。

なので普通に実体を返す方法に変更します。

commit.history.edges?.iter().for_each(|edge| {
  if let Some(edge) = edge {
    if let Some(node) = &edge.node {
      nodes.push(node.clone());
    }
  }
});

Some(nodes)

これでエラーは消えました。
ちなみに、本題とはずれますが、元々nodeにはClone traitは実装してなかったのですが、その場合node.clone()すると参照のコピーが作られ、nodeにClone traitを実装するとnode.clone()で実体のコピーが作られるようになりました。

nodesの参照を返す

最初の例では実体を渡してその中身を抜き出してコピーして返していましたが、参照渡したら参照そのまま返せそうなのでそっちを試してみます。

fn extract_node_data(
    response_data: &commit_query::ResponseData,
) -> Option<Vec<&CommitQueryRepositoryRefTargetOnCommitHistoryEdgesNode>> {
    let repository = response_data.repository?; // エラー
    // 省略
}

以上のようにresponse_dataを参照で渡すと、response_data.repository?で以下のエラーになりました。

cannot move out of `response_data.repository` which is behind a shared reference
move occurs because `response_data.repository` has type `std::option::Option<CommitQueryRepository>`, which does not implement the `Copy` trait

これはOption型の中身を取り出す際に所有権の移動が起きてしまうためエラーが起きているようです。
中身の参照型だけ取り出すにはas_ref()を使います。

fn extract_node_data(
    response_data: &commit_query::ResponseData,
) -> Option<Vec<&CommitQueryRepositoryRefTargetOnCommitHistoryEdgesNode>> {
    let repository = response_data.repository.as_ref()?; 
    // 省略
}

これでエラーが消えました。

まとめ

所有権とかライフタイムとかやっとちょっとだけ理解できました。
特にパフォーマンスは測ってないのですが、実体を取り出す際にはclone()してるので、参照のみ取り出した方がパフォーマンスは良いような気はしてます。

Discussion