せかいや

いまいるここを、おもしろく http://sekai-in-the-box.appspot.com/

【Ruby】OptionParser.newでブロックを使う理由

 
以前パーフェクトRubyの15章では、どうしてOptionParser.newの時にブロックを使ってるのかな。
筆者の趣味かな?って書いていたけれど、


びっくり!

今日、OptionParserの作者さんからメールを貰ったよ!

OptionParser.newのブロックは、中で起きたOptionParser::ParseErrorのメッセージだけをバックトレース抜きで表示します。
コマンドとして使うときにはエラーの内容がわかれば充分で、バックトレースは邪魔になるからです。


 
発生したエラーのうちでも、特にARGV.optionsのブロックの中で起きたエラーの話みたい。
一緒に話をきいてくれた、そらはさんが確認してくれた。


ありがとうそらはさん。男前!


  

バックトレースの抑制

OptionParser::ParseError発生時はバックトレースを抑制する。

通常の(OptionParser::ParseError以外の)エラーの場合

require 'optparse'
parser = OptionParser.new do |opt|
  opt.on('-b') {|v| raise p 10/0 }
end

だらだらとコンソールにエラーが出力される
 
■実行結果

Users/hoge/Desktop/test/commandparser.rb:4:in `/': divided by 0 (ZeroDivisionError)
     from C:/Users/HOGE/Desktop/test/commandparser.rb:4:in `block (2 levels) in <main>'
     from C:/Ruby200/lib/ruby/2.0.0/optparse.rb:1395:in `call'
     from C:/Ruby200/lib/ruby/2.0.0/optparse.rb:1395:in `block in parse_in_order'
     from C:/Ruby200/lib/ruby/2.0.0/optparse.rb:1351:in `catch'
     from C:/Ruby200/lib/ruby/2.0.0/optparse.rb:1351:in `parse_in_order'
     from C:/Ruby200/lib/ruby/2.0.0/optparse.rb:1345:in `order!'
     from C:/Ruby200/lib/ruby/2.0.0/optparse.rb:1437:in `permute!'
     from C:/Ruby200/lib/ruby/2.0.0/optparse.rb:1459:in `parse!'
     from C:/Users/HOGE/Desktop/test/commandparser.rb:7:in `<main>'


 
ParseErrorの場合

require 'optparse'
parser = OptionParser.new do |opt|
  opt.on('-b') {|v| raise OptionParser::ParseError.new }
end

■実行結果
エラートレースがなく、すっきりと出力されている

commandparser.rb:4:in `block (2 levels) in <main>': parse error: -b (OptionParser::ParseError)
        from C:/Users/HOGE/Desktop/test/commandparser.rb:7:in `<main>'

 

エラートレース抑制の理由

そもそもなぜParseErrorだとエラーとレースをしないのか?というと
「バグ」と「実行時例外(想定済みのエラー)」を分けて考える思想に基づくと思われる。
ParseError(実行時例外)の場合に、optparse.rb:1395等 と根深いログまで出る必要は無い。


 
ただし、newメソッドのブロック内でParseErrorが起こる事がほとんどないから、
わざわざブロックを使わなくてもいいのでは、というのが今日の勉強会の皆の見解。

 
個人的にも、
newメソッドの返り値とブロック変数が同じというのは、なんだか煩雑に見える。
この、parser とoptは同じインスタンスを参照している↓
optはブロックスコープ内しか存在しないので、
あえてブロック外で(同じインスタンスを指す)
2つ目の変数parserを利用する必要がある。

parser = OptionParser.new do |opt|
  opt.on('-b') {|v| p opt.object_id, parser.object_id }
end
parser.parse!(ARGV)

■実行結果

23274312
23274312

混乱しちゃう。

ちなみに個人的には、onメソッドのブロック引数内変数 v も
あんまり使われることが無いのに定義されていて分かりにくい。。
 
須藤さん(この記事を書くきっかけになった人。詳しくは後ろに出てくるよ)は
こんなコメント。

私がOptionParser.newのブロックで嫌なのはブロック変数とローカル変数で同じ名前を使うとwarningがでるからなんですよ。

なるほどー。
確かに同じものだったら同じ名前を使いたいかも。
 

ということで、どうするかというとブロック変数では違う名前を使います。ドキュメントではoptsというのを使っています。optsってなんですか!こいつはオプションじゃなくてパーサーなんですよ。optsじゃないんですよ。

須藤さんがおこってる!愛だね。


インスタンス作成をメソッド化することで
ローカル変数を使わない手もあると教えてもらった。
なるほどー。
サンプルコードはこちら
 

 
そもそもどうしてこんなことを掘り返しているかと言うと、
須藤さんの書いたRubyらしいコードとは何か?の記事がきっかけ。

ブロックを使ったほうがよりよい API なのでしょうか。それはより Ruby らしい書き方だからです。

という一文があり、
「なるほど!Rubyらしさを損なわないためにブロックを利用してnewしたのかな」
って考えたんです。

もちろんこの記事ではAPI設計について語っているので
APIを利用している箇所とは微妙に観点が違うとは思いますが、
「より言語の思想に沿う」というあり方なのかなと思って。


 
そしたらさらにびっくり!
須藤さんからもメールを貰ったよ!
 

(ブロックを使うのは)趣味だと思いますよ。
私は多くの場合ではOptionParser.newではブロックを使わないほう
が好みです。

 
だって!
以下色々須藤さんのサンプルコードがあり、思いがあり。。

すごくブログに転送したい。。。けどさすがに気が引ける。
師匠の大阪弁のへんなメールとは訳が違うしね。


(追記)
転送について、MLは全て公開されているからリンク貼っていいよ、とのこと。
なんと!
超かっこいいシステム、ありがとう。。

「ブログに転送したい」とありますが、Asakusa.rbのMLではqwikWebっていうWikiとMLが合体したみたいな超かっこいいシステムを使っているので、 このML上に流れたメールは全てWeb上でアーカイブされています。
件の須藤さんのメールにリンクしたかったらこちらのURLで良いと思います。

http://qwik.jp/asakusarb/307.html#13d15c6b9990d285f5fea44d1b5b5209
ここだよ↑

 

まとめ

普通はブロックで書くといろんなメリットがある
(後処理・前処理・関数渡し・イテレータ、変数のスコープを明確化、処理の塊り感を表現する)けど、
OptionParserはそういったメリットが特に無いという非常に特殊な例だった。

そして、そういったメリットが特に無い場合は、
必ずしもブロックを利用することが「コードの綺麗さ」には繋がらない。
(何が何でもブロックを使うのが正しいわけではない)


 
おまけ

須藤さんの記事はおもしろいです

「どうやったら綺麗なコードが書けるのか」に焦点をあてた須藤さんの記事は、
サンプルコードがたくさんあって、しかも焦点をしぼったコードだから、とても理解しやすくてワクワクする!
ソースコードレビュー公開戦は、あんまり面白くてブログに感想を書いたりしたよ。
こっちは第二回の感想
 
直リンクだとこういう感じだよ
Rubyist Magazine - ライブラリー開発者になろう
Rubyist Magazine - Ruby コードの感想戦 【第 1 回】 WikiR
Rubyist Magazine - Ruby コードの感想戦 【第 2 回】 WikiR


お二人に返すメールの文面を考えていたらこんな時間。。眠い。


 

おまけ

なんと、件のコードを書いた
パーフェクトRubyの作者さんにまでコメントを頂いてしまいました。。!

 
ということでやはり、
sugamasaoさん(筆者さん)の趣味ということで、、めでたしめでたし!