課題6: ライフタイムの実践

マンダトリー要件

問題1:ライフタイムの理解(20点)

以下のコードについて、コンパイルエラーが発生するか判定し、理由を説明しなさい。

// 1-1
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("{}", result);
}

// 1-2
struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael.");
    let excerpt = Excerpt {
        part: &novel[..4],
    };
    drop(novel);
    println!("{}", excerpt.part);
}

// 1-3
fn dangle() -> &str {
    let s = String::from("hello");
    &s
}

// 1-4
fn first_word(s: &str) -> &str {
    &s[..1]
}

// 1-5
struct Context<'a> {
    text: &'a str,
}

impl<'a> Context<'a> {
    fn get_text(&self) -> &str {
        self.text
    }
}

// 1-6
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    x
}

fn main() {
    let string1 = String::from("long");
    {
        let string2 = String::from("short");
        let result = longest(&string1, &string2);
        println!("{}", result);
    }
}

各コードについて:

  • コンパイルできるか?
  • エラーの場合、どのライフタイムルールに違反しているか?
  • どう修正するか?

提出物problem1_lifetime_analysis.md

問題2:ライフタイム注釈の実装(20点)

以下の関数にライフタイム注釈を追加し、実装しなさい。

// 2つの文字列のうち長い方を返す
fn longest(x: &str, y: &str) -> &str {
    // TODO: ライフタイム注釈を追加
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// 最初の単語と残りを返す
fn split_first_word(text: &str) -> (&str, &str) {
    // TODO: ライフタイム注釈を追加
    match text.find(' ') {
        Some(pos) => (&text[..pos], &text[pos + 1..]),
        None => (text, ""),
    }
}

// テキストから最長の行を返す
fn longest_line(text: &str) -> &str {
    // TODO: ライフタイム注釈を追加
    text.lines()
        .max_by_key(|line| line.len())
        .unwrap_or("")
}

// 2つの参照のうち最初のものを返す
fn first_ref<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    // TODO: 実装
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_longest() {
        assert_eq!(longest("hello", "world!"), "world!");
        assert_eq!(longest("rust", "go"), "rust");
    }

    #[test]
    fn test_split_first_word() {
        assert_eq!(split_first_word("hello world"), ("hello", "world"));
        assert_eq!(split_first_word("rust"), ("rust", ""));
    }

    #[test]
    fn test_longest_line() {
        let text = "short\nthis is a longer line\nmedium";
        assert_eq!(longest_line(text), "this is a longer line");
    }

    #[test]
    fn test_first_ref() {
        let x = "first";
        let y = "second";
        assert_eq!(first_ref(x, y), "first");
    }
}

提出物problem2_lifetime_functions.rs

問題3:構造体とライフタイム(20点)

ライフタイムを使った構造体を実装しなさい。

// テキストの一部を保持する構造体
struct TextExcerpt<'a> {
    text: &'a str,
}

impl<'a> TextExcerpt<'a> {
    // 新しいExcerptを作成
    fn new(text: &'a str) -> Self {
        // TODO: 実装
    }

    // テキストを取得
    fn get_text(&self) -> &str {
        // TODO: 実装
    }

    // 最初のn文字を返す
    fn first_n_chars(&self, n: usize) -> &str {
        // TODO: 実装
    }

    // 最初の行を返す
    fn first_line(&self) -> &str {
        // TODO: 実装
    }

    // 単語数を返す
    fn word_count(&self) -> usize {
        // TODO: 実装
    }
}

// 2つの参照を保持する構造体
struct Pair<'a, 'b> {
    first: &'a str,
    second: &'b str,
}

impl<'a, 'b> Pair<'a, 'b> {
    fn new(first: &'a str, second: &'b str) -> Self {
        // TODO: 実装
    }

    fn get_first(&self) -> &str {
        // TODO: 実装
    }

    fn get_second(&self) -> &str {
        // TODO: 実装
    }

    fn longer(&self) -> &str {
        // TODO: 実装
        // ヒント: 返り値のライフタイムはどうなる?
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_text_excerpt() {
        let text = String::from("Hello, Rust programming!");
        let excerpt = TextExcerpt::new(&text);

        assert_eq!(excerpt.get_text(), "Hello, Rust programming!");
        assert_eq!(excerpt.first_n_chars(5), "Hello");
        assert_eq!(excerpt.word_count(), 3);
    }

    #[test]
    fn test_pair() {
        let s1 = String::from("short");
        let s2 = String::from("longer string");

        let pair = Pair::new(&s1, &s2);
        assert_eq!(pair.get_first(), "short");
        assert_eq!(pair.get_second(), "longer string");
        assert_eq!(pair.longer(), "longer string");
    }
}

提出物problem3_lifetime_structs.rs

問題4:実践的なライフタイム活用(20点)

テキスト検索とパース機能を実装しなさい。

// テキスト内でパターンにマッチする行を返す
fn find_lines<'a>(text: &'a str, pattern: &str) -> Vec<&'a str> {
    // TODO: 実装
    // ヒント: patternにはライフタイム不要(返り値に含まれない)
}

// キーと値のペアをパース
fn parse_key_value<'a>(line: &'a str) -> Option<(&'a str, &'a str)> {
    // TODO: 実装
    // 形式: "key=value"
}

// CSVの行をパース
fn parse_csv_line<'a>(line: &'a str) -> Vec<&'a str> {
    // TODO: 実装
    // カンマで分割
}

// 複数行のテキストから指定範囲を抽出
fn extract_lines<'a>(text: &'a str, start: usize, end: usize) -> Vec<&'a str> {
    // TODO: 実装
}

// URLからドメインを抽出
fn extract_domain<'a>(url: &'a str) -> Option<&'a str> {
    // TODO: 実装
    // 例: "https://example.com/path" -> "example.com"
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_find_lines() {
        let text = "hello world\nrust programming\nhello rust";
        let lines = find_lines(text, "hello");
        assert_eq!(lines, vec!["hello world", "hello rust"]);
    }

    #[test]
    fn test_parse_key_value() {
        assert_eq!(parse_key_value("name=Alice"), Some(("name", "Alice")));
        assert_eq!(parse_key_value("invalid"), None);
    }

    #[test]
    fn test_parse_csv_line() {
        let csv = "Alice,30,Engineer";
        assert_eq!(parse_csv_line(csv), vec!["Alice", "30", "Engineer"]);
    }

    #[test]
    fn test_extract_lines() {
        let text = "line1\nline2\nline3\nline4";
        assert_eq!(extract_lines(text, 1, 3), vec!["line2", "line3"]);
    }

    #[test]
    fn test_extract_domain() {
        assert_eq!(extract_domain("https://example.com/path"), Some("example.com"));
        assert_eq!(extract_domain("invalid"), None);
    }
}

提出物problem4_lifetime_parsing.rs

---

ボーナス課題

> ボーナス: 以下はオプションです。マンダトリー要件を完了してから挑戦してください。

ボーナス1:設定ファイルパーサー(5点)

INI形式の設定ファイルパーサーを実装しなさい。

use std::collections::HashMap;

struct Config<'a> {
    sections: HashMap<&'a str, HashMap<&'a str, &'a str>>,
}

impl<'a> Config<'a> {
    fn new() -> Self {
        Config {
            sections: HashMap::new(),
        }
    }

    // INIファイルをパース
    fn parse(text: &'a str) -> Self {
        // TODO: 実装
        // 形式:
        // [section]
        // key=value
    }

    // セクションの値を取得
    fn get(&self, section: &str, key: &str) -> Option<&str> {
        // TODO: 実装
    }

    // セクション一覧を取得
    fn sections(&self) -> Vec<&str> {
        // TODO: 実装
    }
}

fn main() {
    let config_text = r#"
[database]
host=localhost
port=5432

[server]
host=0.0.0.0
port=8080
"#;

    let config = Config::parse(config_text);

    if let Some(host) = config.get("database", "host") {
        println!("Database host: {}", host);
    }
}

提出物bonus1_config_parser/プロジェクト

ボーナス2:トークナイザー(5点)

テキストをトークンに分割するライブラリを実装しなさい。

#[derive(Debug, PartialEq)]
enum Token<'a> {
    Word(&'a str),
    Number(&'a str),
    Symbol(char),
    Whitespace,
}

struct Tokenizer<'a> {
    input: &'a str,
    position: usize,
}

impl<'a> Tokenizer<'a> {
    fn new(input: &'a str) -> Self {
        Tokenizer { input, position: 0 }
    }

    // 次のトークンを取得
    fn next_token(&mut self) -> Option<Token<'a>> {
        // TODO: 実装
    }

    // 全トークンを取得
    fn tokenize(input: &'a str) -> Vec<Token<'a>> {
        // TODO: 実装
    }
}

fn main() {
    let text = "hello 123 + world";
    let tokens = Tokenizer::tokenize(text);

    for token in tokens {
        println!("{:?}", token);
    }
}

提出物bonus2_tokenizer/プロジェクト

ボーナス3:Markdown パーサー(5点)

簡易Markdownパーサーを実装しなさい。

#[derive(Debug, PartialEq)]
enum MarkdownElement<'a> {
    Heading { level: usize, text: &'a str },
    Paragraph(&'a str),
    CodeBlock { language: Option<&'a str>, code: &'a str },
    ListItem(&'a str),
}

fn parse_markdown<'a>(text: &'a str) -> Vec<MarkdownElement<'a>> {
    // TODO: 実装
    // サポート:
    // # Heading
    // ## Heading 2
    // Normal paragraph
    // 
rust // code //
    // - List item
}

fn main() {
    let markdown = r#"
# Title

This is a paragraph.

## Code Example

rust fn main() {}

- Item 1
- Item 2
"#;

    let elements = parse_markdown(markdown);
    for elem in elements {
        println!("{:?}", elem);
    }
}

提出物bonus3_markdown_parser/プロジェクト

ボーナス4:テンプレートエンジン(5点)

シンプルなテンプレートエンジンを実装しなさい。

use std::collections::HashMap;

struct Template<'a> {
    template: &'a str,
}

impl<'a> Template<'a> {
    fn new(template: &'a str) -> Self {
        Template { template }
    }

    // 変数を置換してレンダリング
    fn render(&self, vars: &HashMap<&str, &str>) -> String {
        // TODO: 実装
        // {{name}} -> vars["name"]
    }

    // テンプレート内の変数一覧を取得
    fn variables(&self) -> Vec<&str> {
        // TODO: 実装
    }
}

fn main() {
    let template = Template::new("Hello, {{name}}! You are {{age}} years old.");

    let mut vars = HashMap::new();
    vars.insert("name", "Alice");
    vars.insert("age", "30");

    let result = template.render(&vars);
    println!("{}", result);
}

提出物bonus4_template_engine/プロジェクト

ボーナス5:SQLパーサー(簡易版)(5点)

簡易的なSQLパーサーを実装しなさい。

#[derive(Debug, PartialEq)]
enum SqlStatement<'a> {
    Select {
        columns: Vec<&'a str>,
        table: &'a str,
        where_clause: Option<&'a str>,
    },
    Insert {
        table: &'a str,
        columns: Vec<&'a str>,
        values: Vec<&'a str>,
    },
}

fn parse_sql<'a>(sql: &'a str) -> Option<SqlStatement<'a>> {
    // TODO: 実装
    // サポート:
    // SELECT col1, col2 FROM table WHERE condition
    // INSERT INTO table (col1, col2) VALUES (val1, val2)
}

fn main() {
    let sql = "SELECT name, age FROM users WHERE age > 18";

    if let Some(statement) = parse_sql(sql) {
        println!("{:?}", statement);
    }
}

提出物bonus5_sql_parser/プロジェクト

---

評価基準

マンダトリー部分(80点)

項目 配点 評価ポイント
問題1:ライフタイム理解 20点 ルールの正確な理解
問題2:関数実装 20点 適切なライフタイム注釈
問題3:構造体実装 20点 構造体でのライフタイム活用
問題4:実践的活用 20点 パース処理の実装

ボーナス部分(20点)

項目 配点 評価ポイント
ボーナス1:設定パーサー 5点 実用性
ボーナス2:トークナイザー 5点 状態管理
ボーナス3:Markdownパーサー 5点 複雑な解析
ボーナス4:テンプレート 5点 置換処理
ボーナス5:SQLパーサー 5点 構文解析

: ボーナスは最大20点まで加算されます。

---

提出方法

ファイル構成

rust-foundations-06/
├── problem1_lifetime_analysis.md
├── problem2_lifetime_functions.rs
├── problem3_lifetime_structs.rs
├── problem4_lifetime_parsing.rs
└── bonus/
    ├── bonus1_config_parser/
    ├── bonus2_tokenizer/
    ├── bonus3_markdown_parser/
    ├── bonus4_template_engine/
    └── bonus5_sql_parser/

提出期限

  • マンダトリー:第6章学習後、1週間以内
  • ボーナス:第10章修了時まで

---

ヒント

問題2のヒント

longest関数:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

問題3のヒント

first_line実装:

fn first_line(&self) -> &str {
    self.text.lines().next().unwrap_or("")
}

問題4のヒント

parse_key_value実装:

fn parse_key_value<'a>(line: &'a str) -> Option<(&'a str, &'a str)> {
    let parts: Vec<&str> = line.split('=').collect();
    if parts.len() == 2 {
        Some((parts[0], parts[1]))
    } else {
        None
    }
}

---

学習の確認

この課題を通じて、以下を理解できたか確認してください:

  • [ ] ライフタイムの概念と必要性
  • [ ] ライフタイム注釈の書き方
  • [ ] 構造体でのライフタイム使用
  • [ ] ライフタイム省略規則
  • [ ] 複数のライフタイムパラメータ
  • [ ] 'staticライフタイム
  • [ ] 参照を返す関数の設計

次の章では、型システムを学びます。Rustの豊富な型システムと型推論について理解を深めます。