🦀

[小ネタ] 関数にライフタイム引数を複数定義する必要がある場合の一例

2024/09/01に公開

はじめに

初学者向けです。

Rustで作るプログラミング言語の項2.5を写経中、関数にライフタイムを2種類明示しているコードがありました。なぜライフタイムを2つ指定する必要があるのか疑問に感じ、揃えたところコンパイルエラーが発生しました。指定に意味があることが分かったため調査することにしました。

変更箇所とエラー内容

変更箇所
-fn parse_block<'src, 'a>(input: &'a [&'src str]) -> (Value<'src>, &'a [&'src str]) {
+fn parse_block<'src>(input: &'src [&'src str]) -> (Value<'src>, &'src [&'src str]) {
     let mut tokens = vec![];
     let mut words = input;
エラー内容
   Compiling rustack v0.1.0 (/Users/shuntaka/repos/github.com/shuntaka9576/rust-playground/rustack)
error[E0515]: cannot return value referencing local variable `input`
  --> src/main.rs:39:5
   |
12 |     let mut words = &input[..];
   |                      ----- `input` is borrowed here
...
39 |     stack
   |     ^^^^^ returns a value referencing data owned by the current function

For more information about this error, try `rustc --explain E0515`.
error: could not compile `rustack` (bin "rustack") due to 1 previous error

returns a value referencing data owned by the current function

現在の関数が所有するデータを参照する値を返しています

inputがスコープを抜けるとドロップされます。inputはVecなのでヒープを割り当てていてそこからlineを参照している形だと思います。
単にinputがドロップしたら当然wordもstackも参照が消えるのでコンパイルエラーが起きることは理解出来ます。

word(&[&str]) -> input(Vec<&str>) -> line(&str)
正しく動作するコード全文
use core::panic;

fn main() {
    for line in std::io::stdin().lines().flatten() {
        parse(&line);
    }
}

fn parse(line: &str) -> Vec<Value> {
    let mut stack = vec![];
    let input: Vec<_> = line.split(" ").collect();
    let mut words = &input[..];

    while let Some((&word, mut rest)) = words.split_first() {
        if words.is_empty() {
            break;
        }

        if word == "{" {
            let value;
            (value, rest) = parse_block(rest);
            stack.push(value);
        } else if let Ok(parsed) = word.parse::<i32>() {
            stack.push(Value::Num(parsed));
        } else {
            match word {
                "+" => add(&mut stack),
                "-" => sub(&mut stack),
                "*" => mul(&mut stack),
                "/" => div(&mut stack),
                _ => panic!("{word:?} could not be parsed"),
            }
        }
        words = rest;
    }

    println!("stack: {stack:?}");

    stack
}

fn parse_block<'src, 'a>(input: &'a [&'src str]) -> (Value<'src>, &'a [&'src str]) {
    let mut tokens = vec![];
    let mut words = input;

    while let Some((&word, mut rest)) = words.split_first() {
        if word.is_empty() {
            break;
        }

        if word == "{" {
            let value;
            (value, rest) = parse_block(rest);
            tokens.push(value)
        } else if word == "}" {
            return (Value::Block(tokens), rest);
        } else if let Ok(value) = word.parse::<i32>() {
            tokens.push(Value::Num(value));
        } else {
            tokens.push(Value::Op(word));
        }

        words = rest;
    }

    (Value::Block(tokens), words)
}

#[derive(Debug, PartialEq, Eq)]
enum Value<'src> {
    Num(i32),
    Op(&'src str),
    Block(Vec<Value<'src>>),
}

impl<'src> Value<'src> {
    fn as_num(&self) -> i32 {
        match self {
            Self::Num(val) => *val,
            _ => panic!("Value is not a number"),
        }
    }
}

fn add(stack: &mut Vec<Value>) {
    let lhs = stack.pop().unwrap().as_num();
    let rhs = stack.pop().unwrap().as_num();

    stack.push(Value::Num(lhs + rhs))
}

fn sub(stack: &mut Vec<Value>) {
    let lhs = stack.pop().unwrap().as_num();
    let rhs = stack.pop().unwrap().as_num();

    stack.push(Value::Num(lhs - rhs))
}

fn mul(stack: &mut Vec<Value>) {
    let lhs = stack.pop().unwrap().as_num();
    let rhs = stack.pop().unwrap().as_num();

    stack.push(Value::Num(lhs * rhs))
}

fn div(stack: &mut Vec<Value>) {
    let lhs = stack.pop().unwrap().as_num();
    let rhs = stack.pop().unwrap().as_num();

    stack.push(Value::Num(lhs / rhs))
}

#[cfg(test)]
mod test {
    use super::{parse, Value::*};

    #[test]
    fn test_group() {
        assert_eq!(
            parse("1 2 + { 3 4 }"),
            vec![Num(3), Block(vec![Num(3), Num(4)])]
        )
    }
}

理由

前のコードだと行数が多いので最小コードで考えてみます。このコードでもエラーは再現します。

fn main() {
    let line = "1 str 2";
    let stack = parse(line);
    println!("stack:{stack:?}");
}

fn parse(line: &str) -> Vec<Value> {
    let mut stack = vec![];
    let input: Vec<_> = line.split(" ").collect();
    let words = &input[..];

    let (_word, rest) = words.split_first().expect("word is empty");
    let (value, _rest) = parse_block(rest);

    stack.push(value);

    stack
}

// エラーが起きない記述
// fn parse_block<'src, 'a>(input: &'a [&'src str]) -> (Value<'src>, &'a [&'src str]) {
fn parse_block<'src>(input: &'src [&'src str]) -> (Value<'src>, &[&'src str]) {
    let (&first, rest) = input.split_first().expect("Input is empty");
    if first == "str" {
        (Value::Str(first), rest)
    } else {
        let num = first.parse::<i32>().expect("Failed to parse number");
        (Value::Num(num), rest)
    }
}

#[derive(Debug, PartialEq, Eq)]
enum Value<'src> {
    Num(i32),
    Str(&'src str),
}

parse関数のinputはメソッドのスコープで破棄されます。diffのようにライフタイムを揃えてしまうと、inputとvalueのライフタイムが同じことをコンパイラに伝えてしまいます。inputは前述の通り、parse関数でドロップするので関数が所有するデータを参照する値を返すことになりコンパイルエラーが発生します。
参照元をたどるとlineはmainスコープなので、stackに入れるvalueはinputよりライフタイムが長くて問題ないです。

変更箇所
-fn parse_block<'src, 'a>(input: &'a [&'src str]) -> (Value<'src>, &'a [&'src str]) {
+fn parse_block<'src>(input: &'src [&'src str]) -> (Value<'src>, &'src [&'src str]) {
     let mut tokens = vec![];
     let mut words = input;

以上より、ライフタイム指定子は分けて書く必要がありました。ライフタイムは、どのスコープで定義されたデータを参照しているのかを意識すると正しく指定出来そうな気がしています。今回ですと元はメインスコープのlineですので、parse_blockより長い指定が可能と考えられます。

元コードにはparseにもライフタイム指定子がついています。私のサンプルでは書いてませんが、これは内部的に補完されていることが考えられます。これは「ライフタイムの省略」(Lifetime Elision)と呼ぶようです。

https://github.com/msakuta/rustack/blob/19081dc457e9913f9ffad6ea3c287c13079a868e/examples/03-group.rs#L23

さいごに

書籍には これはソース文字列より短命でありうるため、異なるライフタイムを指定しています と書かれている通りでした。読んだだけではピンときてませんでしたが、色々変更し、納得がいきました。トレイトについているライフタイムと連動しているのかはよく分かっていません🥺

まだまだ初心者なので多分に勘違いを含む可能性があります。忌憚ない意見を頂けたら幸いです。

モヤモヤ

以下にもう少し簡略化したソースコードを示す。これは、コンパイルエラーになる。この原因は関数の戻り値に含まれる参照のライフタイムが明示的に指定されていないためです。おそらくタプルを返却する場合、Rustのコンパイラは戻り値の参照がどの引数から借用されているかを判断できないためです。

エラー内容
   Compiling iroiro v0.1.0 (/Users/shuntaka/repos/github.com/shuntaka9576/rust-playground/iroiro)
error[E0106]: missing lifetime specifiers
  --> src/main.rs:22:53
   |
22 | fn convert_enum_value(str: &str, rest: &[&str]) -> (Value, &[&str]) {
   |                            ----        -------      ^^^^^  ^ ^ expected named lifetime parameter
   |                                                     |      |
   |                                                     |      expected named lifetime parameter
   |                                                     expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `str` or one of `rest`'s 2 lifetimes
help: consider introducing a named lifetime parameter
   |
22 | fn convert_enum_value<'a>(str: &'a str, rest: &'a [&'a str]) -> (Value<'a>, &'a [&'a str]) {
   |                      ++++       ++             ++   ++                ++++   ++   ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `iroiro` (bin "iroiro") due to 1 previous error
ソースコード内容
fn main() {
    let lines = "a1 a2\nb1 b2\nc1 c2";
    let values = split_and_convert_value(lines);

    println!("values: {values:?}")
}

fn split_and_convert_value(lines: &str) -> Vec<Value> {
    let mut values = vec![];

    let inputs: Vec<_> = lines.split("\n").collect();
    let words = &inputs[..];

    let (word, rest) = words.split_first().unwrap();

    let (value, _rest) = convert_enum_value(word, rest);
    values.push(value);

    values
}

fn convert_enum_value(str: &str, rest: &[&str]) -> (Value, &[&str]) {
    (Value::Str(str), rest)
}

#[derive(Debug)]
enum Value<'src> {
    Str(&'src str),
}
ライフタイム1種類のみ指定すると、本記事同様のエラーが発生します。
   Compiling iroiro v0.1.0 (/Users/shuntaka/repos/github.com/shuntaka9576/rust-playground/iroiro)
error[E0515]: cannot return value referencing local variable `inputs`
  --> src/main.rs:19:5
   |
12 |     let words = &inputs[..];
   |                  ------ `inputs` is borrowed here
...
19 |     values
   |     ^^^^^^ returns a value referencing data owned by the current function

For more information about this error, try `rustc --explain E0515`.
error: could not compile `iroiro` (bin "iroiro") due to 1 previous error
同じライフタイムを指定
fn main() {
    let lines = "a1 a2\nb1 b2\nc1 c2";
    let values = split_and_convert_value(lines);

    println!("values: {values:?}")
}

fn split_and_convert_value(lines: &str) -> Vec<Value> {
    let mut values = vec![];

    let inputs: Vec<_> = lines.split("\n").collect();
    let words = &inputs[..];

    let (word, rest) = words.split_first().unwrap();

    let (value, _rest) = convert_enum_value(word, rest);
    values.push(value);

    values
}

fn convert_enum_value<'a>(str: &'a str, rest: &'a [&'a str]) -> (Value<'a>, &'a [&'a str]) {
    (Value::Str(str), rest)
}

#[derive(Debug)]
enum Value<'src> {
    Str(&'src str),
}

ライフタイムを指定することで解消します

ライフタイムを2種類指定し、コンパイルが通るようになったコード
fn main() {
    let lines = "a1 a2\nb1 b2\nc1 c2";
    let values = split_and_convert_value(lines);

    println!("values: {values:?}")
}

fn split_and_convert_value(lines: &str) -> Vec<Value> {
    let mut values = vec![];

    let inputs: Vec<_> = lines.split("\n").collect();
    let words = &inputs[..];

    let (word, rest) = words.split_first().unwrap();

    let (value, _rest) = convert_enum_value(word, rest);
    values.push(value);

    values
}

fn convert_enum_value<'a, 'b>(str: &'a str, rest: &'b [&'a str]) -> (Value<'a>, &'b [&'b str]) {
    (Value::Str(str), rest)
}

#[derive(Debug)]
enum Value<'src> {
    Str(&'src str),
}

タプルを返却せず、ライフタイムは削除し、引数はそのままの場合以下のようなエラーが出る。

この関数(convert_enum_value)の戻り値の型は借用された値を含んでいますが、シグネチャはそれがstrから借用されているのか、restの2つのライフタイムのうちのどれから借用されているのかを示していません
ヘルプ: 名前付きライフタイムパラメータの導入を検討してください

rest引数を削除し、引数を1つにすれば、このエラーは発生しません。

rest引数がない場合、Rustコンパイラは全ての文字列参照が元のlines引数に由来すると推論できます。これにより、返されるValueはlinesと同じライフタイムを持つと安全に判断可能。

rest引数がある場合は、コンパイラはrestがinputs(ローカル変数)から来ていることを知っています。そのため、convert_enum_valueの結果がinputsに依存していると判断せざるを得ません。この依存関係により、ローカル変数inputsへの参照を関数外に持ち出そうとしているとコンパイラが判断し、エラーが発生します。

ライフタイムを同じにした場合は、linesに由来すると推論してくれそうに思えますがしてくれません。最も制限的なライフタイムが想定されると自分は解釈しています。

エラー内容

error[E0106]: missing lifetime specifier
  --> src/main.rs:22:52
   |
22 | fn convert_enum_value(str: &str, rest: &[&str]) -> Value {
   |                            ----        -------     ^^^^^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `str` or one of `rest`'s 2 lifetimes
help: consider introducing a named lifetime parameter
   |
22 | fn convert_enum_value<'a>(str: &'a str, rest: &'a [&'a str]) -> Value<'a> {
   |                      ++++       ++             ++   ++               ++++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `iroiro` (bin "iroiro") due to 1 previous error
ソースコード
fn main() {
    let lines = "a1 a2\nb1 b2\nc1 c2";
    let values = split_and_convert_value(lines);

    println!("values: {values:?}")
}

fn split_and_convert_value(lines: &str) -> Vec<Value> {
    let mut values = vec![];

    let inputs: Vec<_> = lines.split("\n").collect();
    let words = &inputs[..];

    let (word, rest) = words.split_first().unwrap();

    let value = convert_enum_value(word, rest);
    values.push(value);

    values
}

fn convert_enum_value(str: &str, rest: &[&str]) -> Value {
    Value::Str(str)
}

#[derive(Debug)]
enum Value<'src> {
    Str(&'src str),
}

以下のソースコードでも現在の関数が所有するデータを参照する値を返していますエラー。restという値が、inputへの参照を持っていて、wordはlinesを指している可能性もありそう?

ソースコード
fn main() {
    let lines = "a1 a2\nb1 b2\nc1 c2";
    let values = split_and_convert_value(lines);

    println!("values: {values:?}")
}

fn split_and_convert_value(lines: &str) -> &[&str] {
    let inputs: Vec<_> = lines.split("\n").collect();
    let (word, rest) = inputs.split_first().unwrap();

    let rest = convert_enum_value(word, rest);

    rest
}

fn convert_enum_value<'a>(str: &'a str, rest: &'a [&'a str]) -> &'a [&'a str] {
    rest
}

#[derive(Debug)]
enum Value<'src> {
    Str(&'src str),
}

wordを返しても同じエラーなので、違うか...

ソースコード
fn main() {
    let lines = "a1 a2\nb1 b2\nc1 c2";
    let values = split_and_convert_value(lines);

    println!("values: {values:?}")
}

fn split_and_convert_value(lines: &str) -> &&str {
    let inputs: Vec<_> = lines.split("\n").collect();
    let (word, rest) = inputs.split_first().unwrap();

    word
}

fn convert_enum_value<'a>(str: &'a str, rest: &'a [&'a str]) -> &'a [&'a str] {
    rest
}

#[derive(Debug)]
enum Value<'src> {
    Str(&'src str),
}

restを返却して、ライフタイムを指定しても同じエラー。restを返却するかvalueを返却するかで差異はありそう...

ソースコード
fn main() {
    let lines = "a1 a2\nb1 b2\nc1 c2";
    let values = split_and_convert_value(lines);

    println!("values: {values:?}")
}

fn split_and_convert_value(lines: &str) -> &[&str] {
    let inputs: Vec<_> = lines.split("\n").collect();
    let words = &inputs[..];

    let (word, rest) = words.split_first().unwrap();

    let value = convert_enum_value(word, rest);

    value
}

fn convert_enum_value<'a, 'b>(str: &'a str, rest: &'b [&'b str]) -> &'b [&'b str] {
    rest
}

#[derive(Debug)]
enum Value<'src> {
    Str(&'src str),
}

Claude的にはこう、まぁそう理解するか。words.split_first()のwordのスライスのポインタとか見てみたらわかることがあるのかな...

rest を返す場合:
rest は inputs ベクター(関数内でローカルに作成された)のスライスです。したがって、コンパイラは rest のライフタイムが inputs に依存していると判断します。inputs は関数のスコープ内でのみ有効なので、rest を返すことはできません。
Value::Str(str) を返す場合:
str は元の lines パラメータから派生したものとしてコンパイラが推論できます。したがって、Value::Str(str) を返すことは安全だとみなされます。

簡単にいうとrestに'srcをつけると、Value::Strの方も引きずられてrest->inputsの短いライフタイムなるってことかなぁ

Discussion