Ateam Hikkoshi Samurai Inc. Advent Calendar 2017 16日目です。
本日はエイチーム引越し侍の中途6年目、おじさんWebエンジニアの
@dd511805が担当します。
Rubyのブロックについて
Rubyを学びだすと、配列を操作する際に以下の記述を目にすると思います。
ar = [1,2,3,4,5]
ar.each do |el|
puts el
end
# => 1
# 2
# 3
# 4
# 5
この処理を始めて見たときは奇妙な書き方に見えました。もし同じような処理をphpで書くとしたら
以下のような記述になります。foreach構文を使って配列の要素をひとつずつ取り出して処理します
$array = array(1,2,3,4,5);
foreach ($array as &$i) {
print_r $i;
}
javascriptでは以下のように書くことが出来ます。無名関数のおかげでrubyの記述に
近いように見えますが、rubyの記述のdo~endは関数の定義ではありません。
[1,2,3,4,5].forEach((item, i) => {
console.log(item);
});
rubyを学び始めてしばらくの間、このdo~endのまとまりであるブロックについてはなんだか
理解できないけれど、for文の書き方の一種のような認識をしていました。
実際にはfor文の類ではなく、ブロックには多彩な使い方があります。
この記事では、ブロックの使い方のいくつかを紹介しながら、ブロックのレシーバーとなる
メソッドの処理を書いてみるという無意味な車輪の再生産を行いながらブロックの機能について
学びたいと思います。
1. 配列、数値の繰り返しの処理を行う場合
配列であったり、数値型のオブジェクトに対して、決められた回数の繰り返し
処理を実行する際に使われます。下記の記述はArrayクラスのeachメソッド
と同じ様に配列分の繰り返し処理を行うことを考えています。
[1,2,3,4,5].each do |el|
puts el
end
前章の例にも出したとおり、eachメソッドは繰り返しの処理を行うためのメソッドで、
do 〜 endのブロックが引数として渡されて、その処理が繰り返し実行されます。
このような処理を実行する場合は以下のような形でArrayクラスのeachメソッドを
実装します。(似た振る舞いをするメソッドを実装するだけで、同じ動作になること
を保障するものではありません。)
class Array
def each
i = 0
while i < length
yield self[i]
i += 1
end
end
end
まずは、eachメソッドの引数としてブロックを受け取れるようにします。と言いたいところ
ですが、引数と受け取ったブロックをyieldで処理する場合には、eachメソッドに
ブロックを受け取るための引数の定義は必要ありません。
なのでこの処理は自身の配列要素を一つずつ取り出して、yieldの引数に渡すという
ことで実現できます(yieldに渡すself[i]がブロック変数elに対応します)。
実際にはArrayクラスのeachメソッドはRubyでは無くCで実装されてます。
2. リソースの管理をまとめる場合
ブロックは特定の前処理、後処理をまとめることにも使われます。
例えばファイルを開いてそのファイルに内容を書き込んでファイル
を閉じる処理をブロックを使わすに書いてみます。
f = File.open('qiita_todo.txt', 'w')
f << "12/13 09:00 qiitaの記事を読む\n"
f << "12/13 13:00 qiitaの記事を書く\n"
f.close
とくに問題なく、書き込むことが出来ますが、最後のcloseを省略して、本当にやりたい処理
だけ、ブロックとして表現できるようになります。
File.open("qiita_todo_with_block.txt", "w") do |f|
f << "12/13 09:00 qiitaの記事を読む\n"
f << "12/13 13:00 qiitaの記事を書く\n"
end
この処理を実行するためのクラスは以下のようになります。
class File
def self.open(name, mode)
file = new(name, mode)
return file unless block_given?
yield(file)
file.close
end
end
yieldの前にファイルを開いて、それをyieldの引数として実行すること、yieldの後に
ファイルのクローズ処理を加えることで、ブロックにはファイルに対して行いたい処理のみを
記述できるようになりました。またyieldの実行の前にblock_given?メソッドで、ガード
処理をいれることにより、openメソッドに対して、ブロックを渡す場合、渡さない場合
両方とも動作するようになりました。なお実際のメソッドでは、ブロックの評価値が返り値
になるため、動作が少し異なります。
class File
def self.open(name, mode)
file = new(name, mode)
return file unless block_given?
yield(file)
ensure
file.close
end
end
現実の世界に近づけるためには、file.closeをensure式に入れてyieldの結果が返されるよう
にします。
こちらのFileクラスのopenメソッドも現実の世界ではCで実装されています。
3. オブジェクトの初期化と値の設定
次にオブジェクトの初期化にブロックが使用される例について見ていきたいと思います。
Gemのライブラリで以下のような記述があるときがあります。
これもブロックを使うことで記述を綺麗にしています。
MyLib.configure do |config|
config.access_key = ''
config.secret_key = ''
config.region = ''
end
このような場合、内部的には以下のようになっていると推定されます。
configureのメソッドでMyLibに設定すべき値をConfigurationで表現
し、それに対して、ブロックを渡して値の設定を行っています。
module MyLib
class Configuration
attr_accessor :access_key, :secret_key, :region
def initialize
@access_key = nil
@secret_key = nil
@region = nil
end
end
class << self
attr_accessor :configuration
end
def self.configuration
@configuration ||= Configuration.new
end
def self.reset
@configuration = Configuration.new
end
def self.configure
yield(configuration)
end
end
この形にすることにより、Configurationクラスに変数を追加しても、MyLib.configureには
何の変更も加えなくても済みます。そうでない場合には、configurationメソッドの引数を増やす
必要があります。
module MyLib
class << self
attr_accessor :access_key, :secret_key, :region
end
def self.configuration(access_key, secret_key, region)
@access_key = access_key
@secret_key = secret_key
@region = region
end
end
MyLib.configuration(1,2,3)
このような記述の場合だと、変数を増やすたびにメソッドの引数も増やす必要があります。
別の方法としてはconfigureメソッドの引数をハッシュで表現する必要があります。
module MyLib
class << self
attr_accessor :access_key, :secret_key, :region
end
def self.configuration(hash_args)
@access_key = hash_args[:access_key]
@secret_key = hash_args[:secret_key]
@region = hash_args[:region]
end
end
MyLib.configuration({access_key:1, secret_key:2, region:3})
こちらの場合だと、変数が増えてもメソッドの引数は増えないので、その点は良い構造ですが、
ブロックを引数として渡して、その中で変数に値を設定する場合と比べると可読性としては
劣ると思います。
4. 特定のライブラリのDSLの表現
最後に紹介するパターンはDSL表現としてのブロックです。
代表的な例としてはRuby on RailsのSchema.rbがあります。
こちらも実行したい処理を簡潔に表現出来ています。下記の例では
ブロックが2重になっていますが、Rubyのブロックでは括弧等の余計な表記がないため、
行いたい処理を自然に読める形式になっています。
ActiveRecord::Schema.define(version: 20171108074017) do
create_table "categories", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name", comment: "カテゴリ名"
t.bigint "category_id"
end
end
今までのパターンと違う部分としては、第一階層のブロック変数がないことです。
そのため、yieldに対して引数を渡して、ブロック側でそれを受け取ることが出来ません。
このような場合ではinstance_evalメソッドを使ってブロックを
実行します。これによって、レシーバーのオブジェクト(今回の場合ではれば、ActiveRecord::Schema)を
selfとしてブロックを実行できるようになるので、そこで定義されたメソッドcreate_tableも
ブロックとして実行できるようになります。instance_evalメソッドはyieldではないので、
defineメソッドの引数として明示的に&blockを指定して、ブロックをProcオブジェクトとして
受け取ります。
module ActiveRecord
class Schema
def self.define(info = {}, &block)
new.define(info, &block)
end
def define(info, &block)
instance_eval(&block)
end
def create_table(table_name, options = {}, &block)
t = Table.new(table_name, options)
yield(t)
end
end
class Table
def initialize(name, options)
@name = name
@options = options
end
def string(value, options = {})
puts "#{value}カラムをstring型で生成します。"
end
def bigint(value, options = {})
puts "#{value}カラムをbigint型で生成します"
end
def datetime(value, options ={})
puts "#{value}カラムをdatetime型で生成します"
end
end
end
現実の世界ではデータベースにテーブルを作成したりするために使われますが、処理が複雑
なので、メソッドが実行される状態にしたものが上記になります。
2段目のブロックの処理は今迄の形とほぼ同じなので、yieldを使って、表現することが出来ます。
このようにみていくと、処理としては、ブロックの中でメソッドの引数にブロックを渡している
だけなのですが、DSLっぽい記述になっていることがわかります。
まとめ
ブロックの使い方を4つのパターンに分けて見てきましたこのようにしてブロックについて
改めて学んでみると以下のことが分かると思います。
- ブロックはコードの塊である
- ブロックはメソッドに渡され、yieldで実行される
- yieldで実行された引数は、ブロック内部の処理で使うことができる
- メソッドの引数としてブロックが存在しない場合でもメソッドの実行を行い場合にはblock_given?が有効である
- ブロックの処理でインスタンス内のメソッドを実行したい場合にはyieldではなくinstance_eval(&block)を使う。
当然本に書いてあるありきたりな結論なのですが、実際にコードを書いてみることで、
メソッドを作る際に、どのようにブロックを処理すれば、簡潔なコードが書けるかなど、
言葉だけではつかめない感覚的なものを得られるのではないかと思います。
#最後に
Ateam Hikkoshi Samurai Inc. Advent Calendar 2017 16日目いかがでしたでしょうか。
明日はエイチーム引越し侍の新卒webエンジニア、@nrainieroが Angularに関する記事を書いてくれます。
お楽しみに。
追伸
株式会社エイチーム引越し侍では、一緒にサイト改善をしてくれるWebエンジニアを募集しています。エイチームグループのエンジニアとして働きたい!という方は是非、以下のリンクから応募してください。
皆様からのご応募、お待ちしております!!