2015年12月5日土曜日

Ruby でブロックを使ったメソッドを定義してみよう

Ruby Advent Calendar 2015 - Qiita の 12月5日の内容です。

Ruby のブロックとかについて書きます。

毎年アドベントカレンダーがある言語だし、同じような内容がたぶんあると思うんですけど、全部の内容を見直してかぶってないか確認するのとかしんどいので、ブロックのネタ書きます。

普通の使い方


みなさんもよく使うのは Array#each とか Range#each あたりでの使い方ですかね。
たとえば、 1 から 10 の数字を画面に表示するコードとかは下のような内容になります。

(1..10).each do |i|
  puts i
end

このときに使ってる do と end の間にある部分がブロックと言うヤツです。

わりとコイツは便利なんですが、組み込みのクラスや用意されたメソッドなどに対してのブロックは使えるけれど、自分でブロックを与えるメソッドを定義するということに慣れていないユーザが多いような気がするので、今日はその方法について書きます。

ブロックを受け取るメソッド


結論から言ってしまえば、  block_given? というメソッドと yield という式を使えば扱うことができます。
たとえば、整数について偶数か奇数の集合ということを意識したクラスを作ってみます。
名前は EvenOrOddNumbers とします。

class EvenOrOddNumbers << Array
end

ちょっと横着して EvenOrOddNumbers は Array のひとつ、という関係があるとしました。

偶数と奇数を判別する


さて、この EvenOrOddNumbers の Numbers に 偶数がふくまれているかを調べるメソッドと奇数がふくまれているかを調べるメソッドを追加してみましょう。

class EvenOrOddNumbers << Array
  def include_even?
    each do |i|
      return true if i.even?
    end
    false
  end
  def include_odd?
    each do |i|
      return true if i.odd?
    end
    false
  end
end

こんな感じで書くことができます。
利用例は以下のような感じです。


eoon = EvenOrOddNumbers.new
eoon << 1
eoon << 3
eoon << 5
p eoon
puts "include even: #{eoon.include_even?}"
puts "include odd:  #{eoon.include_odd?}"
puts
eoon = EvenOrOddNumbers.new
eoon << 2
eoon << 4
eoon << 6
p eoon
puts "include even: #{eoon.include_even?}"
puts "include odd:  #{eoon.include_odd?}"
puts
eoon = EvenOrOddNumbers.new
eoon << 1
eoon << 2
eoon << 3
p eoon
puts "include even: #{eoon.include_even?}"
puts "include odd:  #{eoon.include_odd?}"



実行結果はこんな感じになります。

[1, 3, 5]
include even: false
include odd:  true
[2, 4, 6]
include even: true
include odd:  false
[1, 2, 3]
include even: true
include odd:  true

ここまでは普通にブロックを使うとこんなメソッドが作れて便利ですよ、みたいな感じで別にブロックを渡すメソッドは作ってないです。

偶数のみや奇数のみを扱うメソッドを作ってみる


本題ですね。偶数のみの each や奇数のみの each を定義してみましょう。

普通はひとつの定義にまとめますが、ブログ的に面倒なので先ほどのクラス定義のあとに以下のコードを記述してもメソッド定義できるので、こんな感じに書いてみました。

class EvenOrOddNumbers
  def each_even(&block)
    each do |i|
      yield i if i.even?
    end
  end
  def each_odd(&block)
    each do |i|
      yield i if i.odd?
    end
  end
end

使い方はこんな感じです。

eoon = EvenOrOddNumbers.new
eoon << 1
eoon << 1
eoon << 2
eoon << 3
eoon << 5
eoon << 8
eoon << 13
puts "EvenOrOddNumbers: #{eoon}"
evens = []
eoon.each_even do |i|
  evens << i
end
odds = []
eoon.each_odd do |i|
  odds << i
end
puts "evens: #{evens}"
puts "odds:  #{odds}"

出力はこんな感じ。

EvenOrOddNumbers: [1, 1, 2, 3, 5, 8, 13]
evens: [2, 8]
odds:  [1, 1, 3, 5, 13]

上の定義の例ではブロックが渡されなかったときの挙動を考えていないのですが、ブロックが渡されなかったときの挙動を記述するには block_given? を使います。

class EvenOrOddNumbers
  def each_even(&block)
    raise 'No block given' unless block_given?
    each do |i|
      yield i if i.even?
    end
  end
  def each_odd(&block)
    raise 'No block given' unless block_given?
    each do |i|
      yield i if i.odd?
    end
  end
end

超適当なんですが、ブロックが渡されなかったとき、いきなり Exception を発生させるという処理を入れるとこんな感じです。

each という名前がつくメソッドだとブロックがなかったときの挙動というのは、どんなものか考えるのか面倒なんで、マネしちゃだめです。

所感


ブロックの扱いについて、複数の引数を受け取るブロック、たとえば Hash#each_with_index とかはどうやって作るか?とか考えると面白いと思います。

今日の内容はこんなところで。

っていうか、最後まで書いて気づいたけど、数年前に同じネタをアドベントカレンダーに書いた気がしてきた・・・まあ、気にしない方針で。

0 件のコメント: