こんにちは!Webコーダー・プログラマーの貝原(@touhicomu)です。
今日は、Rubyのブロックについて書きたいと思います。
「そもそも、ブロックって何?」
そういう疑問が浮かぶほど、ブロックってなかなか理解しずらいでよね。
そういった理由で、「ブロックの使い方がわからない!」という声を、筆者もたびたび耳にします。
確かに、Rubyの文法の中でも、ブロックは、構文も含めて難しいですよね。
そこで!今回は、Rubyのブロックについて、やさしく整理して理解が進むよう解説をしてみました!
この記事では、rubyのブロックについて
・ ブロックとは
・ 繰り返し
・ メソッドの一部変更
という基本的な内容から、
・ ブロックをProcオブジェクトとして受け渡し可能にする
・ ブロックの使用上の注意点(スコープ)
といった応用的な内容についても解説していきます。
ブロック基本編
ブロックとは
ブロックとは、一言で言えば、「引数のかたまり」です。
ブロックの、do~end までの範囲すべてが引数です。
または、{ } で囲まれた範囲もブロックで、この範囲すべてが引数です。
ブロックとはどのようなものなのか、ここで簡単に復習してみましょう。
サンプルコード(do~endのブロックの例):
3.times do | i | x = i * 2 p x end
サンプルコードの実行結果:
0 2 4
上の例の、
“do | i |
x = i * 2
p x
end”
までが、ブロックです。
このブロックが、timesメソッドに引数として、与えられています。
timesメソッドは、このブロックを引数として受け取り、0、1、2と数え上げる時に、数え上げている数を変数iに入れて、引数として受け取ったブロックのコードを実行しています。
そしてブロック内で「p x 」が実行されて、画面に実行結果が表示されています。
サンプルコード( { } のブロックの例):
[ "one", "two", "three" ].each { | n | str = "this num is " + n p str }
サンプルコードの実行結果:
"this num is one" "this num is two" "this num is three"
eachメソッドも、
“{ | n |
str = “this num is ” + n
p str
}”
というブロックを、引数として受け取っています。
eachブロックは、”one”、”two”、”three”と配列をループする時に、ループ中の配列の要素を変数nに入れて、引数として受け取ったブロックのコードを実行しています。
そして、ブロックで、「p str」が実行されて画面に実行結果が表示されています。
このように、ブロックはコードではありますが、あくまで引数なので、単体では存在できません。
繰り返し
繰り返し系のメソッドが、一番ブロックを引数として渡すことの多いケースです。
繰り返しの中でも、eachメソッドが一番多く使われます。
初めのうちは、eachメソッドにブロックを引数として渡すことを練習し、ブロックのコツをつかむ方がいいでしょう。
eachメソッドに慣れてきたら、他にハッシュのeachメソッド、範囲のtimesメソッドなどにもブロックを引数として渡してみてください。
「ブロックを引数として渡す」ということを意識して、実際にコードを書き、エラーが出たら修正していくことで、徐々に「ブロックを引数として渡すことは、こんな感じでOKなんだ」と理解が進み慣れてくると思います。
ここでは、配列に対して、ブロックを引数として渡す練習をしてみましょう。
まずは、基本形であるeachメソッドの例です。
eachメソッドは、配列の要素を数え上げながら、数え上げ中の要素を、ブロック変数numに入れて、引数として渡されたブロックのコードを実行します。
サンプルコード1(each):
array = [1, 2, 3, 4] array.each do |num| p num end
サンプルコード1(each)の実行結果:
1 2 3 4
引数として渡されたブロックで、「p num」が実行されていることが分かりますね。
次によく使う、mapメソッドの例です。
mapメソッドは、ブロック引数の値を新たな配列にして返します。
ブロック引数の値とは、ブロックの最後に実行された式の値です。
以下の例では、「num+10」がブロック引数の値です。
サンプルコード2(map):
array = [1, 2, 3, 4] ret = array.map do |num| num + 10 end p ret
サンプルコード2(map)の実行結果:
[11, 12, 13, 14]
実行結果は、「num+10」された値の配列になっていますね。
次に、これもよく使うselectメソッドの例です。
selectメソッドは、配列の要素を数え上げている中で、数え上げている要素を一つずつブロック変数numに入れて、ブロック引数に渡します。
そして、ブロック引数の値がtrueである要素のみを入れた新たな配列を作成し、selectメソッドの戻り値として返します。
繰り返しますが、ブロック引数の値とは、ブロックの最後に実行された式の値となります。
以下の例では、「num if num % 2 == 0」がブロック引数の値です。
この式は、numが偶数の時はnumの値(trueと等価)、奇数の時はnil(falseと等価)を返します。
そのため、selectメソッドが返す配列は、偶数のみの配列になります。
サンプルコード3(select):
array = [1, 2, 3, 4] ret = array.select do |num| num if num % 2 == 0 end p ret
サンプルコード3の実行結果:
[2, 4]
実行結果では、「num if num % 2 == 0」がtrueとなる要素、つまり偶数の配列が表示されていますね。
ブロック引数のイメージが具体的になってきたでしょうか?
繰り返しますが、「ブロックは引数のかたまり」と思えてきたら、理解が進むと思います。
なお、上記each、map、selectのようなブロック引数を受け取るメソッドのことを「ブロック付きメソッド」と言います。覚えておいてください。
メソッドの一部変更
ここでは、より深く、ブロックについての理解を進めます。
ブロックは、例えば、eachメソッドの処理内容の一部をブロックのコードで変更しているとも言えます。
このブロックによるコード変更のおかげで、柔軟な処理が可能となります。
ここで例をあげましょう。
ここでは、sortメソッドで、ブロックが処理を肩代わりしていることを確認します。
sortメソッドは、文字列の配列などを、アルファベットや50音の昇順で並び替えするメソッドです。
サンプルコード(昇順並び替え):
array = [ "b", "c", "a"] ret = array.sort p ret
サンプルコード(昇順並び替え)の実行結果:
["a", "b", "c"]
ここで、sortメソッドにブロック引数を渡して、sortメソッドの処理の一部を変えましょう。
sortメソッドは、並び替えの基準となる式を「 a <=> b 」という式で使用しています。
この式をブロック引数で変えると、並び替えの順番が変わります。
ここでは、sortメソッドの並び替えを降順にするために、ブロック引数の値を、式「 b <=> a 」としましょう。
「b <=> a 」は、「a <=> b 」と逆の計算結果になるので、ソート結果も昇順の逆の降順になります。
サンプルコード(降順並び替え):
array = ["b", "c", "a"] ret = array.sort do | a, b | b <=> a end p ret
サンプルコード(降順並び替え)の実行結果:
["c", "b", "a"]
並び替えが降順になりましたね。
先にあげたeach、map、selectも同様にブロック引数で処理の一部を変更している例となります。
ブロック応用編
ブロックをProcオブジェクトとして受け渡し可能にする
ブロックはブロック単体では存在を維持できません。
しかし、Procオブジェクトにブロックを渡してあげればProcオブジェクトとして存在できます。
サンプルコード:
proc = Proc.new {|w| puts w} def hello(&proc) proc.call("hello") end hello(&proc)
サンプルコードの実行結果:
hello
Proc.newでインスタンスを作成し、ブロックを渡してあげることで簡単にProcオブジェクトとしてブロックを扱えます。
そして、その後定義したprocをhelloに渡しています。
ProcのインスタンスメソッドであるcallでProcが保持しているブロックを使用することができます。
Procオブジェクトの扱い方はこちらの記事にまとめられているので、一読することをおすすめします。
ブロックの使用上の注意点(スコープ)
最後に、ブロックの注意点を挙げます。
実は、ブロック内の変数は、ブロックの外(do~endの外、{ }の外)では、参照できません。
ブロック内の変数は全てブロックのローカル変数です。このブロックのローカル変数をブロックスコープのローカル変数といいます。
例を挙げて説明しましょう。
サンプルコード:
array = [1, 2, 3, 4] local = 'local' array.each do |num| block = 'block' end p local # => p block # =>
サンプルコードの実行結果
"local" Main.rb:9:in `': undefined local variable or method `block' for main:Object (NameError)
上記サンプルコードのように、変数「local」は、メインスクリプト内のローカル変数ですので、「p」にて正常に値が表示されています。
しかし、変数「block」は、ブロック内のみで有効なので、メインスクリプト内では、未定義のままであり、「p」メソッドで参照するとエラーが出てしまいます。
このようなエラーを出さないために、「ブロック内の変数は、ブロック外では参照できない。」ということを念頭においていてください。
まとめ
いかがでしたでしょうか。
ブロックが引数であるとは、なかなか慣れないものです。
引数のわりには、複数行にまたがって書きますので、「これで引数といっていいの?」など思われるかもしれませんね。
しかし、ブロックはただ単に、{ } や、do ~ end で囲まれた引数なのです。
既存のブロック付きメソッドにブロック引数を与えることで、メソッドの処理を柔軟に、また、直感的に実装できますので、覚えておくととても役に立ちます。
特に、Rubyを書くとブロックは頻出しますので、必ず覚えるようにしてください。
ブロックを覚えると、Rubyが大変使いやすいものになります。
ブロックの使い方を忘れたら、またこの記事を読み返してみてください。