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
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 の重要な特徴:
- エラーは
$_ に格納される($@ ではない)
try と catch はセミコロンで終わる必要がある
- 戻り値を適切に処理できる
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の例外処理をより安全で読みやすくする優れたモジュールです。
重要なポイント:
$@ の問題を回避 - より安全なエラー処理
- finally ブロック - リソースのクリーンアップが確実
- 読みやすい構文 - コードの意図が明確
- セミコロンを忘れずに -
try と catch の後に必要
伝統的な eval も依然として有効ですが、複雑なエラー処理や安全性が重要な場合は Try::Tiny の使用を強くお勧めします。エラーハンドリングを適切に行うことで、堅牢で保守しやすいPerlプログラムを書くことができます。