はじめに
最近、書籍「Effective Ruby」を購入しました。
まだ全部読み終わっていませんが、トリビア的な(?)知識がいろいろ載っていてRuby使いとしてはなかなか面白いです。
その中に catchとthrowを使ったサンプルコードが載っていたんですが、僕の場合catchとthrowって今まで使ったことがありません。
だいたいcatch/throwを使わなくても書けるんですよね~。
Effetive Rubyに載っていたコードもそんなパターンだったので、ちょっと書き直してみることにしました。
License
元のコードはBSD3ライセンスで提供されています。
Copyright and Authors
Copyright (c) 2013, 2014 Peter J. Jones <[email protected]>
元のコードとリファクタリングしたコード
元のコード にあった nested_with_catch
をリファクタリングして、product_with_find
というメソッドを追加してみました。
################################################################################
# This file is part of the package effrb. It is subject to the license
# terms in the LICENSE.md file found in the top-level directory of
# this distribution and at https://github.com/pjones/effrb. No part of
# the effrb package, including this file, may be copied, modified,
# propagated, or distributed except according to the terms contained
# in the LICENSE.md file.
################################################################################
require(File.expand_path('../lib/test.rb', File.dirname(__FILE__)))
##############################################################################
class ThrowTest < MiniTest::Unit::TestCase
# 省略...
##############################################################################
# 本に載っていたコード
def nested_with_catch
player = MiniTest::Mock.new
player.expect(:valid?, true, [String, String])
@characters = %w(Mickey Donald Goofy)
@colors = %w(Black White Red)
# <<: jump
match = catch(:jump) do
@characters.each do |character|
@colors.each do |color|
if player.valid?(character, color)
throw(:jump, [character, color])
end
end
end
end
# :>>
player.verify
return match
end
##############################################################################
# リファクタリングしたコード
def product_with_find
player = MiniTest::Mock.new
player.expect(:valid?, true, [String, String])
@characters = %w(Mickey Donald Goofy)
@colors = %w(Black White Red)
match = @characters.product(@colors).find {|params| player.valid?(*params) }
player.verify
return match
end
##############################################################################
def test_book_code
# 省略...
assert_equal(['Mickey', 'Black'], nested_with_catch)
assert_equal(['Mickey', 'Black'], product_with_find)
end
end
catch/throwって何をしてるの?
catch/throwはラベルを使ってループの大域脱出をしたりするときに使われるRuby標準のメカニズムです。
(JavaやC#のcatch/throwとは意味が異なります。)
match = catch(:jump) do
@characters.each do |character|
@colors.each do |color|
if player.valid?(character, color)
throw(:jump, [character, color])
end
end
end
end
上のコード例では player.valid?(character, color)
がtrueになったときに、throw(:jump, [character, color])
しています。
throw
には :jump
というラベルが付いているので、同じラベルが付いている catch(:jump)
まで処理が戻されます。
さらに、throw
の第2引数に [character, color]
を渡しているので、これが catch
ブロックの戻り値になり、ローカル変数 match
にこの値が格納される、というわけです。
リファクタリングのポイント
僕はcatch/throwを使っていた上のロジックを、次のようにリファクタリングしてみました。
match = @characters.product(@colors).find {|params| player.valid?(*params) }
リファクタリングのポイントを以下で説明します。
eachをネストさせる代わりにproductを使った
やっていることは2つの配列の組み合わせを総当たりでチェックしていくだけなので、Array#product
が使えるなと思いました。
@characters.product(@colors)
# => [["Mickey", "Black"], ["Mickey", "White"], ["Mickey", "Red"], ["Donald", "Black"], ["Donald", "White"], ["Donald", "Red"], ["Goofy", "Black"], ["Goofy", "White"], ["Goofy", "Red"]]
throwで値を返す代わりにfindを使った
やりたいことは2つの配列の中から条件に合う組み合わせを返す、ということなので、Array#find
が使えると思いました。
find
メソッドは配列の中からブロック内の戻り値がtrueになる1件目の要素を返却するメソッドです。
まとめて配列を受け渡しした
find
メソッドは以下のような書き方になっています。
find {|params| player.valid?(*params) }
この書き方にピンと来ない方はこういう書き方に変えるとわかりやすいかもしれません。
find {|character, color| player.valid?(character, color) }
やっていることはどちらも同じで、配列の要素をまとめて受け取ってまとめて渡すか、バラバラに受け取ってバラバラに渡すかの違いになります。
まとめ
というわけで、この記事ではcatch/throwで書いたRubyのコードをproduct/findで書き直してみました。
冒頭でも書いたように、僕は今までcatch/throwを使いたいと思ったことがありません。
catch/throwは一種のgo to文のようなものなので、適切に使わないとプログラムの見通しが悪くなります。
おそらく「catch/throwをどうしても使わないといけない」という場面は滅多にないでしょう。
今回のように別のもっと良い書き方が他にあったり、そもそも根本的にロジックが怪しいというケースが大半なのではないでしょうか。(Effective Rubyでも同じようなことが書いてあります)
「へ~、こんな構文があるんだ!」と思うだけなら良いですが、「新しい知識を仕入れたから積極的に使ってみたい」とは思わないようにしてほしいな~と思い、この記事を書くことにしました。
良かったら参考にしてみてください。
あわせて読みたい(Qiita記事)
Rubyらしいシンプルなコードを書きたい方はこちらの記事が役に立つかもしれません。
[初心者向け] RubyやRailsでリファクタリングに使えそうなイディオムとか便利メソッドとか
あわせて読みたい(本)
まだ読み終わっていませんが、Effective Rubyはなかなか面白い本です。
特に、他の言語からやってきた人が読んだりすると「へ~、Rubyってそんな動きをするのか」という新発見がたくさんあるかもしれません。