Featured image of post Try::Tiny - 例外処理をスマートに

Try::Tiny - 例外処理をスマートに

Perl の例外処理を改善する Try::Tiny の使い方とベストプラクティスを解説します。

Try::Tiny - 例外処理をスマートに

エラー処理は堅牢なプログラムを書く上で欠かせません。Perlでは伝統的に eval を使ってエラーをキャッチしてきましたが、いくつかの落とし穴があります。Try::Tiny は、より安全で読みやすい例外処理を提供するモジュールです。

Perlの伝統的なエラー処理

まず、Perlの基本的なエラー処理方法を振り返ってみましょう。

eval を使った基本的なエラー処理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use feature qw(say);

# 文字列eval(非推奨)
eval "1 / 0";
if ($@) {
    say "エラー: $@";
}

# ブロックeval(推奨)
eval {
    my $result = 1 / 0;
    say "結果: $result";
};

if ($@) {
    say "エラーが発生: $@";
}

# ファイル読み込みの例
eval {
    open my $fh, '<', 'nonexistent.txt' 
        or die "ファイルが開けません: $!";
    # 処理
    close $fh;
};

if ($@) {
    say "ファイル操作エラー: $@";
}

eval の問題点

従来の eval を使ったエラー処理にはいくつかの問題があります:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
use feature qw(say);

# 問題1: $@ が上書きされる可能性
eval {
    eval {
        die "内部エラー";
    };
    # ここで $@ が空になる可能性
    my $x = some_function();  # この中で eval が使われると...
    
    if ($@) {  # $@ が期待した値でないかも
        say "予期したエラー処理";
    }
};

# 問題2: $@ がオブジェクトの場合の真偽値判定
{
    package FalseException;
    use overload bool => sub { 0 }, '""' => sub { "エラー" };
    sub new { bless {}, shift }
}

eval {
    die FalseException->new;
};

if ($@) {  # これは false になってしまう!
    say "エラーを捕捉";
} else {
    say "エラーが見逃される!";  # こっちが実行される
}

# 問題3: $@ がリストコンテキストで予期しない動作
my @result = eval {
    die "エラー";
    return (1, 2, 3);
};

# $@ のチェックよりも先に @result を評価してしまう可能性

Try::Tiny の登場

Try::Tiny はこれらの問題を解決し、より安全で読みやすい例外処理を提供します。

インストール

1
cpanm Try::Tiny

基本的な使い方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use Try::Tiny;
use feature qw(say);

# 基本形
try {
    die "何か問題が発生!";
}
catch {
    say "エラーを捕捉: $_";
};

# 処理の結果を受け取る
my $result = try {
    return 42;
}
catch {
    say "エラー: $_";
    return 0;
};

say "結果: $result";  # 42

# ファイル操作の例
try {
    open my $fh, '<', 'data.txt' or die "ファイルが開けません: $!";
    my $content = do { local $/; <$fh> };
    close $fh;
    say "ファイル内容: $content";
}
catch {
    say "ファイル読み込みエラー: $_";
};

Try::Tiny の重要な特徴:

  • エラーは $_ に格納される($@ ではない)
  • trycatch はセミコロンで終わる必要がある
  • 戻り値を適切に処理できる

finally ブロック

finally ブロックは、エラーの有無にかかわらず必ず実行されます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use Try::Tiny;
use feature qw(say);

my $fh;
try {
    open $fh, '<', 'data.txt' or die "ファイルが開けません: $!";
    # ファイル処理
    my $data = <$fh>;
    say "読み込んだデータ: $data";
}
catch {
    say "エラー: $_";
}
finally {
    if ($fh) {
        close $fh;
        say "ファイルをクローズしました";
    }
};

finally の重要なポイント:

  • エラーの有無にかかわらず実行される
  • catch の前後どちらでも書ける
  • リソースのクリーンアップに最適

エラーの再送出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use Try::Tiny;
use feature qw(say);

sub process_file {
    my ($filename) = @_;
    
    try {
        open my $fh, '<', $filename or die "ファイルが開けません: $!";
        # 処理
        close $fh;
    }
    catch {
        say "ログに記録: $_";
        die $_;  # エラーを再送出
    };
}

try {
    process_file('nonexistent.txt');
}
catch {
    say "上位でキャッチ: $_";
};

エラーハンドリングのベストプラクティス

特定のエラーだけをキャッチ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
use Try::Tiny;
use feature qw(say);

{
    package FileNotFound;
    use overload '""' => sub { shift->{message} };
    sub new { 
        my ($class, $message) = @_;
        bless { message => $message }, $class;
    }
}

{
    package PermissionDenied;
    use overload '""' => sub { shift->{message} };
    sub new {
        my ($class, $message) = @_;
        bless { message => $message }, $class;
    }
}

sub read_file {
    my ($filename) = @_;
    
    # ファイルの存在確認
    die FileNotFound->new("ファイルが見つかりません: $filename")
        unless -e $filename;
    
    # 読み取り権限確認
    die PermissionDenied->new("読み取り権限がありません: $filename")
        unless -r $filename;
    
    open my $fh, '<', $filename or die "ファイルが開けません: $!";
    my $content = do { local $/; <$fh> };
    close $fh;
    
    return $content;
}

try {
    my $content = read_file('/etc/shadow');  # 権限エラーになるはず
    say $content;
}
catch {
    if (ref($_) eq 'FileNotFound') {
        say "ファイルが見つかりません: $_";
        # デフォルトファイルを使用
    }
    elsif (ref($_) eq 'PermissionDenied') {
        say "権限エラー: $_";
        # ユーザーに通知
    }
    else {
        say "予期しないエラー: $_";
        die $_;  # 予期しないエラーは再送出
    }
};

ネストしたtry-catch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use Try::Tiny;
use feature qw(say);

sub outer_function {
    try {
        say "外側の処理開始";
        
        try {
            say "内側の処理開始";
            die "内部エラー";
        }
        catch {
            say "内側でキャッチ: $_";
            # エラーを処理して続行
        };
        
        say "外側の処理続行";
        die "外部エラー";
    }
    catch {
        say "外側でキャッチ: $_";
    };
}

outer_function();

リトライロジック

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
use Try::Tiny;
use feature qw(say);

sub fetch_with_retry {
    my ($url, $max_retries) = @_;
    $max_retries //= 3;
    
    my $attempt = 0;
    my $result;
    
    while ($attempt < $max_retries) {
        $attempt++;
        say "試行 $attempt/$max_retries...";
        
        try {
            # ネットワークリクエストをシミュレート
            die "接続エラー" if rand() > 0.6;
            
            $result = "データ取得成功";
            say "成功!";
            last;  # 成功したらループを抜ける
        }
        catch {
            say "エラー: $_";
            
            if ($attempt >= $max_retries) {
                die "最大リトライ回数に到達: $_";
            }
            
            # 指数バックオフ
            my $wait = 2 ** ($attempt - 1);
            say "${wait}秒待機...";
            sleep $wait;
        };
    }
    
    return $result;
}

try {
    my $data = fetch_with_retry('http://example.com/api', 3);
    say "取得したデータ: $data";
}
catch {
    say "最終的に失敗: $_";
};

実用例

データベーストランザクション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
use Try::Tiny;
use DBI;
use feature qw(say);

sub transfer_money {
    my ($dbh, $from_account, $to_account, $amount) = @_;
    
    try {
        $dbh->begin_work;
        
        # 送金元から引き出し
        my $sth = $dbh->prepare(
            'UPDATE accounts SET balance = balance - ? WHERE id = ?'
        );
        $sth->execute($amount, $from_account);
        die "残高不足" if $sth->rows == 0;
        
        # 送金先に入金
        $sth = $dbh->prepare(
            'UPDATE accounts SET balance = balance + ? WHERE id = ?'
        );
        $sth->execute($amount, $to_account);
        die "送金先アカウントが見つかりません" if $sth->rows == 0;
        
        $dbh->commit;
        say "送金完了: $amount 円";
    }
    catch {
        say "エラー発生: $_";
        say "トランザクションをロールバックします";
        
        try {
            $dbh->rollback;
        }
        catch {
            say "ロールバック失敗: $_";
        };
        
        die $_;  # エラーを再送出
    };
}

# 使用例
my $dbh = DBI->connect('dbi:SQLite:dbname=bank.db', '', '',
    { RaiseError => 1, AutoCommit => 0 });

try {
    transfer_money($dbh, 1, 2, 1000);
}
catch {
    say "送金失敗: $_";
}
finally {
    $dbh->disconnect if $dbh;
};

Web APIリクエスト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
use Try::Tiny;
use HTTP::Tiny;
use JSON::PP;
use feature qw(say);

sub fetch_user_data {
    my ($user_id) = @_;
    
    my $http = HTTP::Tiny->new(timeout => 10);
    my $url = "https://api.example.com/users/$user_id";
    
    my $user_data;
    
    try {
        my $response = $http->get($url);
        
        die "HTTPエラー: $response->{status} $response->{reason}"
            unless $response->{success};
        
        $user_data = decode_json($response->{content});
        
        die "無効なデータ形式"
            unless $user_data->{id} && $user_data->{name};
    }
    catch {
        when (/timeout/) {
            say "タイムアウト: APIサーバーが応答しません";
        }
        when (/HTTPエラー: 404/) {
            say "ユーザーが見つかりません: $user_id";
        }
        when (/HTTPエラー: 5\d\d/) {
            say "サーバーエラー: $_";
        }
        default {
            say "予期しないエラー: $_";
        }
        
        # デフォルト値を返す
        $user_data = { id => $user_id, name => 'Unknown' };
    };
    
    return $user_data;
}

my $user = fetch_user_data(123);
say "ユーザー名: $user->{name}";

ファイル処理の包括的な例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
use Try::Tiny;
use File::Spec;
use File::Path qw(make_path);
use feature qw(say);

sub process_log_file {
    my ($input_file, $output_dir) = @_;
    
    my $input_fh;
    my $output_fh;
    my $processed_count = 0;
    
    try {
        # 入力ファイルを開く
        open $input_fh, '<', $input_file
            or die "入力ファイルが開けません: $!";
        
        # 出力ディレクトリを作成
        make_path($output_dir) unless -d $output_dir;
        
        my $output_file = File::Spec->catfile($output_dir, 'processed.log');
        open $output_fh, '>', $output_file
            or die "出力ファイルが開けません: $!";
        
        # 行ごとに処理
        while (my $line = <$input_fh>) {
            chomp $line;
            
            try {
                # 各行の処理
                next if $line =~ /^#/;  # コメント行はスキップ
                
                # エラー行だけ抽出
                if ($line =~ /ERROR|FATAL/) {
                    print $output_fh "$line\n";
                    $processed_count++;
                }
            }
            catch {
                say "行の処理中にエラー: $_";
                # 個別の行のエラーは続行
            };
        }
        
        say "処理完了: $processed_count 行を出力";
    }
    catch {
        say "重大なエラー: $_";
        die $_;
    }
    finally {
        close $input_fh if $input_fh;
        close $output_fh if $output_fh;
        say "ファイルをクローズしました";
    };
}

try {
    process_log_file('/var/log/application.log', '/tmp/processed_logs');
}
catch {
    say "ログ処理が失敗しました: $_";
};

evalとTry::Tinyの比較

項目 eval Try::Tiny
エラー変数 $@ $_
$@ の上書き問題 あり なし
偽値の例外 問題あり 正しく処理
読みやすさ 中程度 高い
finally ブロック なし あり
パフォーマンス 高速 わずかに遅い

いつ何を使うべきか

eval を使うべき場合

  • パフォーマンスが極めて重要
  • シンプルなエラー処理で十分
  • Try::Tiny の依存を避けたい

Try::Tiny を使うべき場合

  • 複雑なエラー処理が必要
  • 安全性を重視
  • 読みやすいコードを書きたい
  • finally ブロックが必要

まとめ

Try::Tiny は、Perlの例外処理をより安全で読みやすくする優れたモジュールです。

重要なポイント:

  1. $@ の問題を回避 - より安全なエラー処理
  2. finally ブロック - リソースのクリーンアップが確実
  3. 読みやすい構文 - コードの意図が明確
  4. セミコロンを忘れずに - trycatch の後に必要

伝統的な eval も依然として有効ですが、複雑なエラー処理や安全性が重要な場合は Try::Tiny の使用を強くお勧めします。エラーハンドリングを適切に行うことで、堅牢で保守しやすいPerlプログラムを書くことができます。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。