Blog::kobaken

prove t/foo/bar/baz.t

Perlで、少しの記述ミスでよく起こるエラーの意味とその対処と予防

Perlに限らない話ですが、プログラミングをしているとセミコロンが抜けた、タイポなど些細な記述ミスでエラーが出ますよね。エラーメッセージから、原因を明確に特定できれば対処はしやすいですが、現実は、そうではなく、この辺が怪しい...くらいしか分からないことも多々。慣れたプログラミング言語でなければ、当然、土地勘がなくエラーの意味を汲み取れず、逆に慣れていれば「あーそれね」とすぐ解決に辿り着くのものです。

この記事では、Perlの初学者を対象に、Perlでよくある記述ミスとエラー、その対処方法、そして、そもそもエラーを起きにくくするための予防策をできる限り書きます。

いろんな記述ミスを紹介する前に

perldiagを利用する

エラーメッセージの意味は、perldocのperldiagを見ると全て書いてあります。この記事では、よく遭遇するだろうエラーに絞り、説明も簡易です。見知らぬエラーに出会ったり、より詳細な情報が知りたい場合は、perldiagの参照をおすすめします。

use strict; use warnings あるいは use VERSIONを利用する

この記事で示すコードは、use strict; use warnings;と書いている前提で書きます。この2つのプラグマを最低限利用していないと、より不可解な不具合に遭遇します。もしperl5.36以降を利用しているのであれば、この2つのプラグマよりもuse v5.36;use v5.38のようにuse VERSION文を利用することをおすすめします。v5.36以降の場合、この2つのプラグマを有効にしつつ、さらに間接オブジェクト記法などハマりがちな記法を禁止してくれます。

use VERSIONと書くのがオススメ

それでは、紹介していきます。

can't use string as a hash ref

このエラーは、文字列をハッシュリファレンスとして扱おうとした時のエラーです。次のコードでは、(x,y)座標から半径を計算しようとしていますが、記述ミスがあり、このエラーが発生しています。

sub radius {
    my $point = @_;
    return sqrt($point->{x} ** 2 + $point->{y}**2);
    # => Can't use string ("1") as a HASH ref while "strict refs" in use
}

radius({x => 3, y => 4});

このコードの記述ミスは、関数の引数の受け取り箇所です。2行目のmy $point = @_;my ($point) = @_と修正すれば、ひとまずエラーは回避できます。原因は、引数配列の@_スカラーコンテキストで受け取っているため、$pointには引数の配列長の1が入り、$point->{x}は、"1"->{x}と評価されエラーになっています。難しいですね。これを理解するには、Perlのコンテキストを学ぶ必要がありますが、次の記事がオススメです。

blog.kondoyoshiyuki.com

引数配列の@_の処理に慣れないうちは、sub 関数名(引数)のように関数定義ができるsubroutine signatures機能がおすすめです。他の言語でよく見る書き方ですね。次のコードのように、引数配列の@_を書くことなく、引数を受け取れるので、上記のエラーに悩ませることは減ると思います。

use v5.38; # これで subroutine signatures 機能が有効化されます

sub radius($point) {
    return sqrt($point->{x} ** 2 + $point->{y}**2);
}

radius({x => 3, y => 4});

subroutine signatures機能を有効化するには、use feature 'signatures'と書くか、v5.36以降のperlであれば、use v5.36のようにuse VERSIONを書きます。長らく実験的な機能という扱いでしたが、v5.36から正式な機能になっています。

関数の引数受け取り以外の場面でこのエラーに遭遇することはありますが、その場合、エラーの意味通り、文字列とハッシュリファレンスの取り違えを直すことになると思います。

Global symbol "$hello" requires explicit package name (did you forget to declare "my $hello"?)

このエラーは、多くの場合、タイポしている時に発生します。このエラーの意味は、グローバル変数$helloを利用するなら$main::helloのようにパッケージ名も含めて完全に修飾が必要だと言っています。括弧の中身が気の利いた指摘で、my $helloといった宣言忘れてない?と言っています。

このエラーが発生している次のコードの場合、1行目のmy $halloが意図しない命名なので、my $helloと修正すれば対処できます。

my $hallo = 'Hello, world!';
print $hello; # => Global symbol "$hello" requires explicit package name (did you forget to declare "my $hello"?)

静的解析ツールを導入してタイポを検出する確率を上げるのは、良い予防になると思います。具体的には、Perl::Criticという静的解析ツールにPerl::Critic::Policy::Variables::ProhibitUnusedVarsStricterを追加すれば、未使用の変数があると次のような警告が出ます。(タイポした変数名が、偶然他の変数名と一致した場合は、未使用な変数ではなくなり検出できないので、完璧な予防ではないです。)

未使用の変数の警告をしている

Perl::Criticについては、次の記事が詳しいです。

blog.utgw.net

Can't locate object method "catch" via package "1" (perhaps you forgot to load "1"?)

このエラーは、次のコードで発生したエラーです。

try {
    # do something
} catch { # => Can't locate object method "catch" via package "1" (perhaps you forgot to load "1"?)
    # handle error
    print "Error: $@";
};

エラーメッセージを直訳すれば、catchメソッドがパッケージの"1"にないという意味です。が、このコードの何が"1"なのか?なぜcatchメソッド呼び出しが行われているのか?といった疑問が湧くと思います。解説は次の記事を読んでください。そして、このエラーの対処は、use Try::Tiny; とすれば解決します。

techblog.karupas.org

このエラーをわかりにくくしている原因は、Foo->method(...) といったメソッド呼び出しをmethod Foo(...)とも書けるようにする間接オブジェクト記法です。この例だと、catch PackageNameといった間接オブジェクト記法の解釈がされて、PackageName->catch(...)と同じ状態です。そして、PackageNameは、{ print "Error: $@" }の返り値の1が採用され、'1'->catchと解釈されてます。やはり詳しくは上記のブログを読んでほしいです。

この記法はno feature 'indirect' または v5.36以降で、use VERSIONと書けば禁止できます。禁止すると次のコードのように、ただの文法エラーに変わり、エラー意図がわかりやすくなります。*1

use v5.38;

try { # => syntax error at foo.pl line 3, near "try {"
   # do something
} catch {
     # handle error
    print "Error: $@";
}

Useless use of a constant ("xxx") in void context

この警告は、値が変数などにいれることなく、void contextで呼ばれ、何にも利用されない値があった場合に発生します。例えば、use strict; use warnings; "hello"と書いたperlスクリプトで発生します。ここではより悩ましい例を紹介します。次のテストコードの意図は、Hello->messageの呼び出し結果が、'hello' と一致するかテストしているだけで意図は自明だと思います。ですが、この警告がでます。

package Hello {
    sub message { 'hello' }
}

use Test::More;

is Hello->message, 'hello'; # => Useless use of a constant ("hello") in void context

done_testing;

これも原因は間接オブジェクト記法です。is Hello->message, 'hello'が、Hello->is->message, 'hello'と解釈されてしまっているせいです。簡易的な対処はis(Hello->message, 'hello');のようにカッコを使って評価順を明確にしたり、is +Hello->message, 'hello'is 'Hello'->message, 'hello' のようにスカラコンテキストを強制して、期待通りの動作させられます。

しかし、そもそもの曖昧な解釈の原因になっている間接オブジェクト記法を禁止するのが良い予防だと思います。具体的には、次のコード冒頭のようにno feature 'indirect'するか use VERSIONすれば解決します。

use v5.38; # あるいは no feature 'indirect'; 

package Hello {
    sub message { 'hello' }
}

use Test::More;

is Hello->message, 'hello';

done_testing;

Can't call method "xxx" on an undefined value

未定義値からメソッドを呼び出してしまったエラーです。次のコードの場合は、publishedとすべきところ、pubilshedとタイポしているため、このエラーが発生しています。

use DateTime;
my $row = { published => DateTime->now };

$row->{pubilshed}->ymd; # => Can't call method "ymd" on an undefined value

これくらい簡単な構造であれば、目で追いやすいですが、次のコードのように構造が複雑になってくると、どこに問題があるかわかりづらく厄介です。

use DateTime;
my $data = { foo => { bar => [ { baz => { now => DateTime->now } } ] } };

say $data->{foo}{bar}[0]{bazz}{now}->ymd; # => Can't call method "ymd" on an undefined value

簡易なデバッグ方法があります。存在しない要素にアクセスした時にエラーになるように一時的にリファレンスを読み込み専用に変更してしまいます。例えば、Data::Lockのdlock関数を使えば、次のコードのように存在しないキーbazzにアクセスしたことを教えてくれます。

use Data::Lock qw(dlock);
use constant DEBUG => $ENV{DEBUG} || 0;

dlock($data) if DEBUG;
say $data->{foo}{bar}[0]{bazz}{now}->ymd; # Attempt to access disallowed key 'bazz' in a restricted hash

予防ですが、複雑な構造をいっぺんに処理しようとせず、処理を分割すると問題の切り分けがしやすくなって良いと思います。*2

Foo.pm did not return a true value

Perlのモジュールは、モジュールを正常に読み込まれたことを示すために、その最後に真を返す必要があります。Foo.pm did not return a true valueというエラーは、モジュールが最後に真を返さなかったか、あるいは読み込みの途中でエラーが発生し中断された場合に発生します。大抵の場合は、1; のつけ忘れなので、まず最初にモジュールの末尾に1;があるか確認すると良いでしょう。

package Foo;
use strict;
use warnings;

sub hello { print "Hello!" }

# 多くの場合、末尾に1; と書くがその記述がないため、
# Foo.pmをrequireすると、Foo.pm did not return a true value というエラーに。

予防は、モジュール末尾の1;をそもそも書かなくてもよくすることです。これは、v5.38からmodule_trueというfeatureが入ったので、そちらを利用すると良いです。module_trueは、ただ次のようにv5.38と書くだけで、有効化され、期待通りモジュールを読み込みできます。

package Foo;
use v5.38;

sub hello { print "Hello!" }

syntax error at foo.pl line X, near "print"

このエラーは"print"付近で何かしら文法が間違っていると言っています。言い過ぎなところはありますが、大抵、このエラーの原因はセミコロンのつけ忘れです。以下のコードであれば、エラーはprint付近であると言っているので前の行にセミコロンをつけ忘れていないか見ると良さそうです。

my $hello = 'Hello, world!'
print $hello; # => syntax error at foo.pl line X, near "print"

my $helloの文に終わりにセミコロンを打って、対処できます。

my $hello = 'Hello, world!';
print $hello;

おわりに

この記事を書くキッカケは、Perlのエラーメッセージをそのまま検索窓に貼り付け検索し、このブログに辿り着いてる人がいることがわかったからです。この記事が、Perlのエラーの対処や予防に少しでも繋がれば嬉しいです。また、最近のPerlを利用するとエラーを特定しやすくなる場面が多いので、バージョンアップのキッカケにもなればとも思っています。これまで、use strict; use warnings;を添えるのがベストプラクティスとされていましたが、今は、use v5.38;といったuse VERSIONと書くことをオススメしたいです。

以上です!

*1:tryが未定義ゆえにsyntaxエラーになっている。

*2:処理の分割をどうやって行うか考えることは、本質的な問題ですよね。