Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Rubyをやっていてブロック処理(do 〜 end)がちゃんとわかってない人は、怒らないから見ていきなさい

More than 3 years have passed since last update.

Ateam Hikkoshi Samurai Inc. Advent Calendar 2017 16日目です。
本日はエイチーム引越し侍の中途6年目、おじさんWebエンジニアの
@dd511805が担当します。

Rubyのブロックについて

Rubyを学びだすと、配列を操作する際に以下の記述を目にすると思います。

array.rb
ar = [1,2,3,4,5]
ar.each do |el|
  puts el
end
# => 1
#    2
#    3
#    4
#    5

この処理を始めて見たときは奇妙な書き方に見えました。もし同じような処理をphpで書くとしたら
以下のような記述になります。foreach構文を使って配列の要素をひとつずつ取り出して処理します

array.php
$array = array(1,2,3,4,5);
foreach ($array as &$i) {
  print_r $i;
}

javascriptでは以下のように書くことが出来ます。無名関数のおかげでrubyの記述に
近いように見えますが、rubyの記述のdo~endは関数の定義ではありません。

array.js
[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. リソースの管理をまとめる場合

ブロックは特定の前処理、後処理をまとめることにも使われます。
例えばファイルを開いてそのファイルに内容を書き込んでファイル
を閉じる処理をブロックを使わすに書いてみます。

write_file.rb
f = File.open('qiita_todo.txt', 'w')
f << "12/13 09:00 qiitaの記事を読む\n"
f << "12/13 13:00 qiitaの記事を書く\n"
f.close

とくに問題なく、書き込むことが出来ますが、最後のcloseを省略して、本当にやりたい処理
だけ、ブロックとして表現できるようになります。

write_file_with_block.rb
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

この処理を実行するためのクラスは以下のようになります。

file.rb
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メソッドに対して、ブロックを渡す場合、渡さない場合
両方とも動作するようになりました。なお実際のメソッドでは、ブロックの評価値が返り値
になるため、動作が少し異なります。

file.rb
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のブロックでは括弧等の余計な表記がないため、
行いたい処理を自然に読める形式になっています。

schema.rb
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オブジェクトとして
受け取ります。

activerecord.rb
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エンジニアを募集しています。エイチームグループのエンジニアとして働きたい!という方は是非、以下のリンクから応募してください。
皆様からのご応募、お待ちしております!!

エイチームグループ採用サイト(Web開発エンジニア職)

dd511805
a-hikkoshi
当社は、親会社である株式会社エイチームの経営理念をそのままに、引越しの比較サービス開始以降、大切にしてきた「三方よし」の理念を基本として、世の中に求められるサービスの創造を目指します。 一緒に働けるエンジニアを募集しております。下記URLよりご応募ください。 https://bit.ly/3lwf7QJ
https://hikkoshi.a-tm.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away