Skip to content

Part III: エラーハンドリングと Option/Result

本章では、関数型プログラミングにおける安全なエラーハンドリングを学びます。null や例外に頼らず、Option<T>Result<T, E> を使って型安全にエラーを扱う方法を習得します。


第6章: Option 型による安全なエラーハンドリング

6.1 なぜ Option が必要か

従来のエラーハンドリングには問題があります。

ソースファイル: app/rust/src/ch06_option.rs

uml diagram

問題のあるコード(unwrap を使用)

// panic! が発生する可能性あり
pub fn parse_show_unsafe(raw_show: &str) -> (&str, i32, i32) {
    let bracket_open = raw_show.find('(').unwrap();  // パニックの可能性
    let bracket_close = raw_show.find(')').unwrap(); // パニックの可能性
    let dash = raw_show.find('-').unwrap();          // パニックの可能性

    let name = raw_show[..bracket_open].trim();
    let year_start: i32 = raw_show[bracket_open + 1..dash].parse().unwrap();
    let year_end: i32 = raw_show[dash + 1..bracket_close].parse().unwrap();

    (name, year_start, year_end)
}

// 正常ケース
parse_show_unsafe("Breaking Bad (2008-2013)");  // OK

// 異常ケース → panic!
parse_show_unsafe("Chernobyl (2019)");  // パニック! '-' がない
parse_show_unsafe("The Wire 2002-2008"); // パニック! '(' がない

6.2 Option の基本

Option<T> は「T 型の値があるか、ないか」を表す型です。

uml diagram

#[derive(Debug, Clone, PartialEq)]
pub struct TvShow {
    pub title: String,
    pub start: i32,
    pub end: i32,
}

impl TvShow {
    pub fn new(title: &str, start: i32, end: i32) -> Self {
        Self {
            title: title.to_string(),
            start,
            end,
        }
    }
}

6.3 TV番組のパース例

Option を使って安全にパースする方法を学びます。

小さな関数から組み立てる

/// 番組名を抽出
pub fn extract_name(raw_show: &str) -> Option<String> {
    let bracket_open = raw_show.find('(')?;  // ? 演算子で None を伝播
    if bracket_open > 0 {
        Some(raw_show[..bracket_open].trim().to_string())
    } else {
        None
    }
}

/// 開始年を抽出
pub fn extract_year_start(raw_show: &str) -> Option<i32> {
    let bracket_open = raw_show.find('(')?;
    let dash = raw_show.find('-')?;

    if dash > bracket_open + 1 {
        let year_str = &raw_show[bracket_open + 1..dash];
        year_str.parse().ok()  // parse の Result を Option に変換
    } else {
        None
    }
}

/// 終了年を抽出
pub fn extract_year_end(raw_show: &str) -> Option<i32> {
    let dash = raw_show.find('-')?;
    let bracket_close = raw_show.find(')')?;

    if bracket_close > dash + 1 {
        let year_str = &raw_show[dash + 1..bracket_close];
        year_str.parse().ok()
    } else {
        None
    }
}

uml diagram

6.4 ? 演算子による合成

Rust の ? 演算子は、Scala の for 内包表記に相当します。

/// TV番組をパース(Option 版)
pub fn parse_show(raw_show: &str) -> Option<TvShow> {
    let name = extract_name(raw_show)?;
    let year_start = extract_year_start(raw_show)
        .or_else(|| extract_single_year(raw_show))?;
    let year_end = extract_year_end(raw_show)
        .or_else(|| extract_single_year(raw_show))?;

    Some(TvShow::new(&name, year_start, year_end))
}

// 正常ケース
parse_show("Breaking Bad (2008-2013)");  // Some(TvShow { ... })
parse_show("Chernobyl (2019)");          // Some(TvShow { ... })

// 異常ケース → None が返される(パニックなし)
parse_show("The Wire 2002-2008");         // None

6.5 or_else によるフォールバック

or_else を使って、最初の Option が None の場合に代替を試すことができます。

let seven: Option<i32> = Some(7);
let eight: Option<i32> = Some(8);
let none: Option<i32> = None;

assert_eq!(seven.or(eight), Some(7));   // 最初が Some なのでそのまま
assert_eq!(none.or(eight), Some(8));    // 最初が None なので代替を使用
assert_eq!(seven.or(none), Some(7));
assert_eq!(none.or(none), None);

単年の番組に対応する

/// 単年の番組から年を抽出(例: "Chernobyl (2019)")
pub fn extract_single_year(raw_show: &str) -> Option<i32> {
    // ダッシュがある場合は単年ではない
    if raw_show.contains('-') {
        return None;
    }

    let bracket_open = raw_show.find('(')?;
    let bracket_close = raw_show.find(')')?;

    if bracket_close > bracket_open + 1 {
        let year_str = &raw_show[bracket_open + 1..bracket_close];
        year_str.parse().ok()
    } else {
        None
    }
}

// これで単年の番組もパースできる
parse_show("Chernobyl (2019)");  // Some(TvShow { title: "Chernobyl", start: 2019, end: 2019 })

6.6 Option の主要メソッド

メソッド 説明
map 値があれば変換 Some(5).map(\|x\| x * 2)Some(10)
and_then 値があれば Option を返す関数を適用 Some(5).and_then(\|x\| Some(x * 2))Some(10)
filter 条件を満たさなければ None Some(5).filter(\|&x\| x > 10)None
or / or_else None なら代替を使用 None.or(Some(5))Some(5)
unwrap_or None ならデフォルト値 None.unwrap_or(0)0
ok_or Result に変換 Some(5).ok_or("error")Ok(5)
let year: Option<i32> = Some(996);
let no_year: Option<i32> = None;

// map
assert_eq!(year.map(|y| y * 2), Some(1992));
assert_eq!(no_year.map(|y| y * 2), None);

// and_then (Scala の flatMap 相当)
assert_eq!(year.and_then(|y| Some(y * 2)), Some(1992));
assert_eq!(no_year.and_then(|y| Some(y * 2)), None);

// filter
assert_eq!(year.filter(|&y| y < 2020), Some(996));
assert_eq!(year.filter(|&y| y > 2020), None);

// unwrap_or
assert_eq!(year.unwrap_or(0), 996);
assert_eq!(no_year.unwrap_or(0), 0);

6.7 エラーハンドリング戦略

複数の要素をパースする場合、2つの戦略があります。

uml diagram

Best-effort 戦略

pub fn parse_shows_best_effort(raw_shows: &[&str]) -> Vec<TvShow> {
    raw_shows
        .iter()
        .filter_map(|raw| parse_show(raw))  // Some のみ収集
        .collect()
}

let raw_shows = vec![
    "Breaking Bad (2008-2013)",
    "The Wire 2002 2008",        // 無効な形式
    "Mad Men (2007-2015)",
];

let shows = parse_shows_best_effort(&raw_shows);
// vec![TvShow("Breaking Bad", ...), TvShow("Mad Men", ...)]
// 無効なものは無視される

All-or-nothing 戦略

pub fn parse_shows_all_or_nothing(raw_shows: &[&str]) -> Option<Vec<TvShow>> {
    raw_shows.iter().map(|raw| parse_show(raw)).collect()
}

// 全部成功 → Some(vec![...])
parse_shows_all_or_nothing(&["Breaking Bad (2008-2013)", "Mad Men (2007-2015)"]);
// Some(vec![TvShow(...), TvShow(...)])

// 一つでも失敗 → None
parse_shows_all_or_nothing(&["Breaking Bad (2008-2013)", "Invalid"]);
// None

6.8 forall と exists 相当

Scala の forallexists に相当する操作を実装できます。

/// Option に対する forall
pub fn option_forall<T, F>(opt: &Option<T>, predicate: F) -> bool
where
    F: Fn(&T) -> bool,
{
    opt.as_ref().map(predicate).unwrap_or(true)
}

/// Option に対する exists
pub fn option_exists<T, F>(opt: &Option<T>, predicate: F) -> bool
where
    F: Fn(&T) -> bool,
{
    opt.as_ref().map(predicate).unwrap_or(false)
}

let year: Option<i32> = Some(996);
let no_year: Option<i32> = None;

// forall: 「全て」または「存在しない」
assert!(option_forall(&year, |&y| y < 2020));     // true
assert!(option_forall(&no_year, |&y| y < 2020));  // true (None は true)

// exists: 「存在して条件を満たす」
assert!(option_exists(&year, |&y| y < 2020));     // true
assert!(!option_exists(&no_year, |&y| y < 2020)); // false (None は false)

第7章: Result 型と複合的なエラー処理

7.1 Option の限界

Option は「値があるかないか」しか表現できません。なぜ失敗したのかを伝えられません。

ソースファイル: app/rust/src/ch07_result.rs

uml diagram

7.2 Result の基本

Result<T, E> は「T 型の成功値か、E 型のエラーか」を表す型です。

  • Ok(value): 成功
  • Err(error): 失敗(エラー情報を保持)
pub fn extract_name(raw_show: &str) -> Result<String, String> {
    let bracket_open = raw_show
        .find('(')
        .ok_or_else(|| format!("Can't find '(' in {}", raw_show))?;

    if bracket_open > 0 {
        Ok(raw_show[..bracket_open].trim().to_string())
    } else {
        Err(format!("Can't extract name from {}", raw_show))
    }
}

extract_name("The Wire (2002-2008)");  // Ok("The Wire")
extract_name("(2022)");                // Err("Can't extract name from (2022)")

7.3 Result を使ったパース

pub fn extract_year_start(raw_show: &str) -> Result<i32, String> {
    let bracket_open = raw_show
        .find('(')
        .ok_or_else(|| format!("Can't find '(' in {}", raw_show))?;
    let dash = raw_show
        .find('-')
        .ok_or_else(|| format!("Can't find '-' in {}", raw_show))?;

    if dash > bracket_open + 1 {
        let year_str = &raw_show[bracket_open + 1..dash];
        year_str
            .parse()
            .map_err(|_| format!("Can't parse '{}' as year", year_str))
    } else {
        Err(format!("Can't extract start year from {}", raw_show))
    }
}

extract_year_start("The Wire (2002-2008)");  // Ok(2002)
extract_year_start("The Wire (-2008)");      // Err("Can't extract start year from ...")
extract_year_start("The Wire (oops-2008)");  // Err("Can't parse 'oops' as year")

7.4 Option から Result への変換

ok_or / ok_or_else メソッドで OptionResult に変換できます。

let year: Option<i32> = Some(996);
let no_year: Option<i32> = None;

assert_eq!(year.ok_or("no year given"), Ok(996));
assert_eq!(no_year.ok_or("no year given"), Err("no year given"));

uml diagram

7.5 Result による完全なパーサー

pub fn parse_show(raw_show: &str) -> Result<TvShow, String> {
    let name = extract_name(raw_show)?;
    let year_start = extract_year_start(raw_show)
        .or_else(|_| extract_single_year(raw_show))?;
    let year_end = extract_year_end(raw_show)
        .or_else(|_| extract_single_year(raw_show))?;

    Ok(TvShow::new(&name, year_start, year_end))
}

parse_show("The Wire (2002-2008)");  // Ok(TvShow { ... })
parse_show("Mad Men ()");            // Err("Can't extract single year from ...")
parse_show("(2002-2008)");           // Err("Can't extract name from ...")

7.6 Result の主要メソッド

メソッド 説明
map Ok の値を変換 Ok(5).map(\|x\| x * 2)Ok(10)
and_then Ok なら Result を返す関数を適用 Ok(5).and_then(\|x\| Ok(x * 2))Ok(10)
or_else Err なら代替を使用 Err("err").or_else(\|_\| Ok(5))Ok(5)
ok Option に変換 Ok(5).ok()Some(5)
map_err Err の値を変換 Err("e").map_err(\|e\| format!("Error: {}", e))
let year: Result<i32, &str> = Ok(996);
let no_year: Result<i32, &str> = Err("no year");

// map
assert_eq!(year.map(|y| y * 2), Ok(1992));
assert_eq!(no_year.map(|y| y * 2), Err("no year"));

// and_then
assert_eq!(year.and_then(|y| Ok(y * 2)), Ok(1992));
assert_eq!(no_year.and_then(|y| Ok(y * 2)), Err("no year"));

// ok (Option に変換)
assert_eq!(year.ok(), Some(996));
assert_eq!(no_year.ok(), None);

7.7 代数的データ型(ADT)- enum

Rust の enum を使って、より表現力の高いモデルを作れます。

直和型(Sum Type)

複数の選択肢を OR で表す型です。

/// 音楽ジャンル
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MusicGenre {
    HeavyMetal,
    Pop,
    HardRock,
    Jazz,
}

/// 活動期間
#[derive(Debug, Clone, PartialEq)]
pub enum YearsActive {
    /// 現在も活動中
    StillActive { since: i32 },
    /// 活動終了
    ActiveBetween { start: i32, end: i32 },
}

直積型(Product Type)

複数のフィールドを AND で組み合わせる型です。

/// アーティスト
#[derive(Debug, Clone, PartialEq)]
pub struct Artist {
    pub name: String,
    pub genre: MusicGenre,
    pub origin: String,
    pub years_active: YearsActive,
}

uml diagram

7.8 パターンマッチング

直和型は パターンマッチング で処理します。

/// アーティストが指定期間に活動していたか判定
pub fn was_artist_active(artist: &Artist, year_start: i32, year_end: i32) -> bool {
    match &artist.years_active {
        YearsActive::StillActive { since } => *since <= year_end,
        YearsActive::ActiveBetween { start, end } => *start <= year_end && *end >= year_start,
    }
}

/// アーティストの活動年数を計算
pub fn active_length(artist: &Artist, current_year: i32) -> i32 {
    match &artist.years_active {
        YearsActive::StillActive { since } => current_year - since,
        YearsActive::ActiveBetween { start, end } => end - start,
    }
}

let metallica = Artist::new(
    "Metallica",
    MusicGenre::HeavyMetal,
    "U.S.",
    YearsActive::StillActive { since: 1981 }
);

let led_zeppelin = Artist::new(
    "Led Zeppelin",
    MusicGenre::HardRock,
    "England",
    YearsActive::ActiveBetween { start: 1968, end: 1980 }
);

assert!(was_artist_active(&metallica, 1990, 2000));
assert!(!was_artist_active(&led_zeppelin, 1990, 2000));

assert_eq!(active_length(&metallica, 2024), 43);
assert_eq!(active_length(&led_zeppelin, 2024), 12);

uml diagram

7.9 検索条件のモデリング

検索条件も enum でモデリングできます。

/// 検索条件
#[derive(Debug, Clone, PartialEq)]
pub enum SearchCondition {
    SearchByGenre(Vec<MusicGenre>),
    SearchByOrigin(Vec<String>),
    SearchByActiveYears { start: i32, end: i32 },
}

/// アーティストを検索
pub fn search_artists<'a>(
    artists: &'a [Artist],
    conditions: &[SearchCondition],
) -> Vec<&'a Artist> {
    artists
        .iter()
        .filter(|artist| {
            conditions.iter().all(|condition| match condition {
                SearchCondition::SearchByGenre(genres) => genres.contains(&artist.genre),
                SearchCondition::SearchByOrigin(locations) => locations.contains(&artist.origin),
                SearchCondition::SearchByActiveYears { start, end } => {
                    was_artist_active(artist, *start, *end)
                }
            })
        })
        .collect()
}

7.10 カスタムエラー型

エラー種別を enum で定義することで、より型安全なエラーハンドリングができます。

/// パースエラー
#[derive(Debug, Clone, PartialEq)]
pub enum ParseError {
    MissingName,
    MissingYear,
    InvalidYear(String),
    MissingBracket,
}

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ParseError::MissingName => write!(f, "Missing name"),
            ParseError::MissingYear => write!(f, "Missing year"),
            ParseError::InvalidYear(s) => write!(f, "Invalid year: {}", s),
            ParseError::MissingBracket => write!(f, "Missing bracket"),
        }
    }
}

/// TV番組をパース(カスタムエラー型版)
pub fn parse_show_typed(raw_show: &str) -> Result<TvShow, ParseError> {
    let bracket_open = raw_show.find('(').ok_or(ParseError::MissingBracket)?;

    if bracket_open == 0 {
        return Err(ParseError::MissingName);
    }

    // ... 以下省略
}

parse_show_typed("Breaking Bad (2008-2013)");  // Ok(TvShow { ... })
parse_show_typed("(2008-2013)");               // Err(ParseError::MissingName)
parse_show_typed("Test ()");                   // Err(ParseError::MissingYear)

7.11 支払い方法の例

より実践的な enum の例として、支払い方法をモデリングします。

/// 支払い方法
#[derive(Debug, Clone, PartialEq)]
pub enum PaymentMethod {
    CreditCard { number: String, expiry: String },
    BankTransfer { account_number: String },
    Cash,
}

/// 支払い方法を説明
pub fn describe_payment(method: &PaymentMethod) -> String {
    match method {
        PaymentMethod::CreditCard { number, .. } => {
            format!("Credit card ending in {}", number)
        }
        PaymentMethod::BankTransfer { account_number } => {
            format!("Bank transfer to account {}", account_number)
        }
        PaymentMethod::Cash => "Cash payment".to_string(),
    }
}

assert_eq!(
    describe_payment(&PaymentMethod::CreditCard {
        number: "1234".to_string(),
        expiry: "12/25".to_string()
    }),
    "Credit card ending in 1234"
);
assert_eq!(describe_payment(&PaymentMethod::Cash), "Cash payment");

まとめ

Part III で学んだこと

uml diagram

Scala との比較

概念 Scala Rust
値の有無 Option[A] Option<T>
成功/失敗 Either[E, A] Result<T, E>
値あり Some(value) Some(value)
値なし None None
成功 Right(value) Ok(value)
失敗 Left(error) Err(error)
for 内包表記 for { ... } yield ? 演算子
flatMap flatMap and_then
orElse orElse or_else
Option→Either toRight ok_or
ADT sealed trait + case class enum

Option vs Result の使い分け

状況 使用する型
値があるかないかだけが重要 Option<T>
失敗理由を伝える必要がある Result<T, E>
検索結果が見つからない Option<T>
バリデーションエラーを伝える Result<T, String>
複数のエラー種別がある Result<T, ErrorEnum>

キーポイント

  1. Option: 値の有無を型で表現する
  2. Result: 成功/失敗とエラー情報を型で表現する
  3. ? 演算子: Option/Result を組み合わせて使う(Scala の for 内包表記相当)
  4. or_else: フォールバックを提供する
  5. ADT: enum でドメインを正確にモデリング
  6. パターンマッチング: enum を安全に処理する(網羅性チェックあり)

次のステップ

Part IV では、以下のトピックを学びます:

  • 非同期処理(async/await)
  • ストリーム処理(tokio-stream)

演習問題

問題 1: Option の基本

以下の関数を実装してください。

fn safe_divide(a: i32, b: i32) -> Option<i32> {
    todo!()
}

// 期待される動作
assert_eq!(safe_divide(10, 2), Some(5));
assert_eq!(safe_divide(10, 0), None);
assert_eq!(safe_divide(7, 2), Some(3));
解答
fn safe_divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        None
    } else {
        Some(a / b)
    }
}

問題 2: Option の合成

以下の関数を実装してください。2つの数値文字列を受け取り、その合計を返します。

fn add_strings(a: &str, b: &str) -> Option<i32> {
    todo!()
}

// 期待される動作
assert_eq!(add_strings("10", "20"), Some(30));
assert_eq!(add_strings("10", "abc"), None);
assert_eq!(add_strings("xyz", "20"), None);
解答
fn add_strings(a: &str, b: &str) -> Option<i32> {
    let x: i32 = a.parse().ok()?;
    let y: i32 = b.parse().ok()?;
    Some(x + y)
}

問題 3: Result によるバリデーション

以下の関数を実装してください。年齢を検証し、エラーメッセージを返します。

fn validate_age(age: i32) -> Result<i32, String> {
    todo!()
}

// 期待される動作
assert_eq!(validate_age(25), Ok(25));
assert_eq!(validate_age(-5), Err("Age cannot be negative".to_string()));
assert_eq!(validate_age(200), Err("Age cannot be greater than 150".to_string()));
解答
fn validate_age(age: i32) -> Result<i32, String> {
    if age < 0 {
        Err("Age cannot be negative".to_string())
    } else if age > 150 {
        Err("Age cannot be greater than 150".to_string())
    } else {
        Ok(age)
    }
}

問題 4: パターンマッチング

以下の enum とパターンマッチングを使った関数を実装してください。

enum PaymentMethod {
    CreditCard { number: String, expiry: String },
    BankTransfer { account_number: String },
    Cash,
}

fn describe_payment(method: &PaymentMethod) -> String {
    todo!()
}

// 期待される動作
assert_eq!(
    describe_payment(&PaymentMethod::CreditCard {
        number: "1234".to_string(),
        expiry: "12/25".to_string()
    }),
    "Credit card ending in 1234"
);
assert_eq!(
    describe_payment(&PaymentMethod::BankTransfer {
        account_number: "9876".to_string()
    }),
    "Bank transfer to account 9876"
);
assert_eq!(describe_payment(&PaymentMethod::Cash), "Cash payment");
解答
fn describe_payment(method: &PaymentMethod) -> String {
    match method {
        PaymentMethod::CreditCard { number, .. } => {
            format!("Credit card ending in {}", number)
        }
        PaymentMethod::BankTransfer { account_number } => {
            format!("Bank transfer to account {}", account_number)
        }
        PaymentMethod::Cash => "Cash payment".to_string(),
    }
}