Ruby

Effective Rubyのcatch/throwをproduct/findで書き換える

More than 1 year has passed since last update.

はじめに

最近、書籍「Effective Ruby」を購入しました。
まだ全部読み終わっていませんが、トリビア的な(?)知識がいろいろ載っていてRuby使いとしてはなかなか面白いです。

その中に catchとthrowを使ったサンプルコードが載っていたんですが、僕の場合catchとthrowって今まで使ったことがありません。
だいたいcatch/throwを使わなくても書けるんですよね~。

Effetive Rubyに載っていたコードもそんなパターンだったので、ちょっと書き直してみることにしました。

License

元のコードはBSD3ライセンスで提供されています。

https://github.com/pjones/effrb/blob/master/LICENSE.md

Copyright and Authors

Copyright (c) 2013, 2014 Peter J. Jones <pjones@devalot.com>

元のコードとリファクタリングしたコード

元のコード にあった 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ってそんな動きをするのか」という新発見がたくさんあるかもしれません。

Effective Ruby
image