
> この点ははっきり言おう。こういう形でreturnできるのはperlの美点だと。
途中で return できるのは別に perl 独自の機能ではないですよね。「perl
なら許される」ということであれば、ちょっと理解できない。どんな言語で
あろうと、基本は出口はひとつがベストと考えます。ただし実際にプログラム
を組むと、例外処理・エラー処理が必要になるので、なかなかそうもいかない。
ではどういう return なら容認するかというと、わたしの場合は
sub func {
return if $debug;
...
}
という関数先頭付近で値のチェックをして return するタイプ。あるいは
関数の途中であっても、
sub func {といったように、後から読んだ人に
...
$ret = subfunc1();
return if ( ! $ret );
$ret = subfunc2();
return if ( ! $ret );
$ret = subfunc3();
return if ( ! $ret );
...
}
「この関数内ではこういう戻り方をする」
「だから新たなチェックをする場合は、この書き方を真似しろよ。
if とか使うなよ」
とアピールしているなら OK。小飼さんのソースからは、書き手の意思やメッセージが
見えない (しいて言うなら「俺は return にはこだわらない」かな?)。書き手の意思を
こめられない場合は、if 文を使うべきだと思います。
で、この場合は return 云々以前に、そもそも関数の切り出し方がイマイチなのでは
ないでしょうか。search_result 関数は
- Yahoo! にリクエストを投げてレスポンスを取得
- レスポンスを解析
- 取り出したデータに OL・LI 要素を付加して返す
という順序凝縮になっており、「この関数は何をしてるの?」という問いに対して、
「~です」とスパッと答えることができない。だから結果的に return に無理が出てくる。
最初に見たときは「OL 要素というオブジェクトを返す関数」かなと思ったのですが、
よく見ると
('x hits', '<ol>', '<li>...</li>', '<li>...</li>', '</ol>')
と先頭にヒット数がついた配列を返しているので、ちょっと違う。
> そして、今回は関数化はもう一つの重要な利点を帯びている。
> それは、関数search_result()が、このサンプルコードの「ホット
> スポット」だからだ。読者が注意して読むべきなのは、そこだけ
> なのだ。あとは定数の設定、モジュールの宣言、結果の出力(print)
> といった「日常茶飯事」であり、読み飛ばしてかまわないところなのだ。
なるほど、そういう視点はありませんでした。しかし、ホットスポットと
いいつつも query が指定されていなければ return したり、HTML 生成を
行っていたり、いまいちホットスポットにしきれていない。トータルで
読みやすくなっているのか? というと、ちょっと疑問です。
結局のところ、仮にホットスポット化によってわかりやすくなるとしても、
return がいまいち、関数の切り出し方がいまいち、という結果になるなら、
関数化しないという選択肢も決して間違いではないでしょう。
なお、仕事でやるなら
$response = get_response()と機能凝縮で分けます。しかし今回のソース短さでは引数・戻り値の受け渡しが
($hit, @data) = parse_response($response)
煩雑になるだけで、関数に切り出すメリットがありません。
で、わたしが一番気になるのは、雑誌の読者がこのソースを理解できるの?
ということです。まぁ、はてなユーザが相手なら問題ないのかもしれませんが、
たとえばラクダ本の存在すら知らなくて、CGI&Perlポケットリファレンス
で全部済ませてしまう人も多いわけです。
そういう人たちに map・URI.pm・CGI.pm をふんだんに盛り込んだソース
を与えても、意味がわからんと投げ出す人・ぱっと見て読もうともしない人が
少なからずいるような気がします。そうなると、Yahoo! API の使い方を伝える
という本来の目的が達成できない。
特に
return
$xml->{'totalResultsAvailable'}, "hits",
ol(map {
encode_utf8 li(a({href=>$_->{'ClickUrl'}}, $_->{'Title'}))
} @{ $xml->{'Result'} } );
を読んで、foreach ( @{ $xml->{'Result'} } ) でまわす書き方に書き
換えられない人は結構多いんじゃないでしょうか。
「そういう読者はターゲットにしていない」というのもひとつの考え方ですが、
あまりにもったいないなぁと。
最初のソースで解説しておいて、より美しいソースはコレだぜ! という展開
なら納得できますが、最初からリファクタリング後のソースを出すのはどう
なんでしょう。小飼さんのソースで編集からの OK が出ますか? (わたしは
雑誌執筆経験がないので、純粋な疑問です)
> それにしても惜しい。「無精で短気で傲慢なプログラマ」さんは、こうした
> 議論を進めるにあたって、サンプルコードを用意すべきであった。私のそれは
> きちんと動作検証までしてある(もちろんAPPIDは私のに変えてある)。少々
> 傲慢さが足りないのではないだろうか。
blog のタイトルで「俺って優秀だぜ」と自慢するのもおこがましいので当初は、
「無精で短気で傲慢なプログラマを目指して」
としようかと思ったのですが、長かったので削ったのです。なので無精な
わけではないのです :-)
で、わたしの場合は、元のソースの方がわかりやすいと思いました。ただし
わたしなりの色を出すとすれば以下のようにします。おもしろみのないソースで
すみません。
[狙いと言い訳]
- 目的は「Yahoo! API の使い方の解説」を読者に伝えること。
- 読者を選ばないため、できる限りモジュールを使わない。
- import せず LWP::Simple::get() と書くことで、モジュールを知らない読者が
混乱しないよう配慮。
- デリファレンス結果を一度配列 @results に格納し、リファレンスをわかって
いない読者に配慮。
- Model と View の分離をちょっとだけ主張してみる。
- Model から View のデータ受け渡しの @click_urls と @titles は非常にかっこ
悪いが、Class::Struct を使ったり、クラスをうほどでもないので、ここは妥協。
(多分なにかしら適当なモジュールがあると思いますが、知らないので)
- LWP::Simple::get に失敗した場合は致命的エラーなので die。
fatalsToBrowser で画面に表示。改造時の利便性を考慮。
- parse_args は、インタフェース仕様書を兼ねている。プログラム内で必要な
入力はここで全部用意しろよ、途中で直接環境変数を参照したりするなよ、
サニタイズするならここでやれよ、という意図を込めたつもり。
- 一度も動かしてません。構文チェックもしてません。申し訳ない。
#!/usr/local/bin/perl
use strict;
use warnings;
use LWP::Simple;
use XML::Simple;
use Encode;
use CGI::Carp qw(fatalsToBrowser);
my $webapi_baseurl = 'http://api.search.yahoo.co.jp/WebSearchService/V1/webSearch';
my $myydn_appid = '..';
my $max_results = 10;
my $query = parse_args($ENV{'QUERY_STRING'});
my $hit_num;
my @click_urls;
my @titles;
if ( defined $query ){
# リクエストURIの生成
my $req_url = "$webapi_baseurl?appid=$myydn_appid&query=$key&results=$max_results";
# APIへリクエストを送信
my $yahoo_response = LWP::Simple::get($req_url) or die "$!";
# 取得したXMLをパースする
my $xmlsimple = XML::Simple->new();
my $yahoo_xml = $xmlsimple->XMLin($yahoo_response, ForceArray=>['Result']);
# 件数取得
$hit_num = $yahoo_xml->{'totalResultsAvailable'};
# URL とタイトルを取得
my @results = @{$yahoo_xml->{'Result'}};
foreach my $result (@results){
push(@click_urls, $result->{'ClickUrl'});
push(@titles, $result->{'Title'});
}
}
print "Content-type: text/html; charset=utf8\n";
print "\n";
print "<html><body>\n";
print "<form><input type='text' name='query'><input type='submit' value='search'></form>\n";
if ( defined $query ){
print "$hit_num hits\n";
print "<ol>\n";
for ( my $i=0 ; $i<@click_urls ; $i++ ){
printf("<li><a href='%s'></a></li>\n",
$click_urls[$i],
Encode::encode_utf8($titles[$i]));
}
print "</ol>\n";
}
print "</body>\n";
print "</html>\n";
exit 0;
#--------
sub parse_args {
my ($query_string) = @_;
my $l_query = undef;
foreach ( split('&', $query_string) ){
my ($key, $value) = split('=', $_);
$value =~ tr/+/ /;
$value =~ s/(\W)/"%" . unpack("H2", $1)/ge;
if ( $key eq 'query' ){
$l_query = $value;
}
}
return $l_query;
}


