Help us understand the problem. What is going on with this article?

Rubyのdefine_methodについて

More than 1 year has passed since last update.

はじめに

以下のようなコードを見て、difine_methodの使われ方が良く分からなかったため、調べてみました。
特に、ブロックパラメーターがたくさん使われているため、define_methodの後にあるブロックパラメーター i が何を意味しているのか理解できませんでした。

【訂正】当初の記事ではブロック内の| |で囲まれた部分(下記のコード例ではwordやnum、iのこと)を「ブロック引数」と表現していましたが、正しくは「ブロックパラメーター」でした。本文内の記述を訂正させて頂きました。
コメントを頂いた@scivolaさん、ありがとうございました!

合わせて、初学者の方の参考になればと思い、コード内で使われている記法やメソッドについて初心者目線で解説してみました。コードを読んでも何をしているか分からない方の参考になればと思います。

sample.rb
NUMBERS = %w(zero one two three four five six seven eight nine)

NUMBERS.each_with_index do |word, num|
  define_method word do |i = nil|
    i ? num * i : num
  end
end

p two      #=> 2
p two(2)   #=> 4
p nine     #=> 9
p nine(3)  #=> 27

%wについて

リファレンス
https://docs.ruby-lang.org/ja/2.5.0/doc/spec=2fliteral.html#percent

Rubyには%記法というものがあり、文字列、配列やハッシュ等々を%記号を使って記載することができます。

# 通常の配列の書き方
p ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
#=> ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]

# %記法を使った書き方
p %w(zero one two three four five six seven eight nine)
#=> ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]

どちらも同じ結果を得られますが、%記法を使用した方が簡潔に記載することが出来ます。
上記のコードでは%wを使用して、zeroからnineまでの配列を作成し定数NUMBERSに代入しています。

Enumerable#each_with_indexについて

リファレンス
https://docs.ruby-lang.org/ja/2.5.0/method/Enumerable/i/each_with_index.html

each_with_index -> Enumerator
each_with_index {|item, index| ... } -> self

each_with_indexメソッドは通常のeachメソッドのように繰り返しを行いながら、各要素にインデックス番号を付番することが出来ます。ブロックを渡す場合はブロックパラメーターの第一引数(item)に各要素が順番に入り、第二引数(index)にインデックス番号が0番から自動で代入されていきます。

NUMBERS = %w(zero one two three four five six seven eight nine)

NUMBERS.each_with_index do |word, num|
  # 省略
end
#  word   num
# "zero"   0
#  "one"   1
#      省略
# "nine"   9

上記のコードでは%w記法を使って定義した配列NUMBERSに対してeach_with_indexを使用しているため、ブロックパラメーターの第一引数(word)に配列NUMBERSの各要素("zero"から"nine"まで)が順番に代入され、それぞれの要素に対して第二引数(num)のインデックス番号が0から順番に代入されています。
従って、"zero"には0のインデックスが、"one"には1のインデックスが付番され、最後の"nine"まで繰り返し行われます。

Module#define_methodについて

リファレンス
https://docs.ruby-lang.org/ja/2.5.0/method/Module/i/define_method.html

define_method(name, method) -> Symbol
define_method(name) { ... } -> Symbol

define_methodは第一引数(name)に指定した名称をメソッド名とし、第二引数(method)にそのメソッドの処理内容を指定します。
また、第二引数にブロックを渡した場合は、ブロック内の処理が第一引数(name)で指定したメソッドの処理内容になります。このブロック内の処理については、第一引数で作成したメソッドを呼び出した際に初めて実行されます。

NUMBERS = %w(zero one two three four five six seven eight nine)

NUMBERS.each_with_index do |word, num|
  define_method word do |i = nil|
   # 省略
  end
end

上記のコードの場合は、define_methodの第一引数としてwordを指定しています。このwordが何かと言うと、前述したeach_with_indexの第一引数に指定されていたwordです。each_with_indexのwordには配列NUMBERSの各要素が順番に代入されていました。従って、define_methodの第一引数にwordが指定されていると言うことは、配列NUMBERSの各要素の名称をメソッド名として定義している事になります。つまり、"zero"と言う名称のメソッドから"nine"と言う名称のメソッドがここで順番に定義されています。
各メソッドの処理内容についてはブロックを渡しています。前述したように、ブロック内の処理は定義したメソッドが呼び出されるまで実行はされません。(例えばtwoメソッドを呼び出した時に初めてブロック内の処理が実行されます)。またブロックパラメーターとしてiを指定しており、そのデフォルト値をnilにしています。
最後にブロック内の処理内容を確認します。

条件演算子(三項演算子)について

リファレンス
https://docs.ruby-lang.org/ja/2.5.0/doc/spec=2foperator.html#cond
Rubyには条件演算子と言うものがあります(他の言語にもあります)

文法:
式1 ? 式2 : 式3
式1の結果によって式2または式3を返します。
if 式1 then 式2 else 式3 end
とまったく同じです。

リファレンスに書いてある通りなのですが、if文をこのように書くことが出来ます。
?と:で式が分けられています。式1は条件式に該当するので、真(true)か偽(false)を返す式を記載します。
式1が真なら式2が実行されます。式1が偽なら式3が実行されます。

NUMBERS = %w(zero one two three four five six seven eight nine)

NUMBERS.each_with_index do |word, num|
  define_method word do |i = nil|
    i ? num * i : num
  end
end

上記のコードではdefine_methodのブロック内で条件演算子が使用されています。
i と (num * i) と num が?と:で区切られているのが分かると思います。
ここで、Rubyでは「nilとfalse以外は全て真とされる」という決まりがあります。
参考
https://docs.ruby-lang.org/ja/2.5.0/class/FalseClass.html

false は nil オブジェクトとともに偽を表し、 その他の全てのオブジェクトは真です。

上記の条件演算子をもう一度見てみると、式1にはブロックパラメーターのiしか記載されていませんが、iにfalseかnil以外の値が代入されていれば(真ならば)式2(num * 1)が実行され、iにfalseかnilが代入されていれば(偽ならば)式3(num)が実行されます。ここで、iにはデフォルト値としてnilが設定されているため(ブロックパラメーター内で i = nilとなっている部分)今回の条件演算子は、

(式1が真)iにfalse以外の値が入れば
(式2を実行)num(each_with_indexで付番したwordに対応するインデックス番号)にiを掛けた数値を返す。
(式1が偽)iに何も指定がなければ(デフォルト値のままなら)
(式3を実行)numをそのまま返す事になります。

まとめ

sample.rb
NUMBERS = %w(zero one two three four five six seven eight nine)

NUMBERS.each_with_index do |word, num|
  define_method word do |i = nil|
    i ? num * i : num
  end
end

p two      #=> 2
p two(2)   #=> 4
p nine     #=> 9
p nine(3)  #=> 27

最初のコードをもう一度見てみます。特に最後の4行。
twoを呼び出すと2が返ってきます。これは、define_method wordの部分で定義したtwoメソッドを引数なしで呼び出しています。引数の指定が無いため、ブロックパラメーターiにはデフォルト値のnilが代入されます。するとブロック内の条件演算子ではiは偽と判断されるので、twoメソッドに対応するインデックスの2がそのまま返ってきています。
two(2)を呼び出すと4が返ってきます。今回は引数2を指定してtwoメソッドを呼び出しています。この引数2はdefine_methodの部分でwordのブロックパラメーターであるiに代入されます。

twoというメソッドはwordの部分で定義されており、その処理内容は続くブロックの中に記載されます。考え方は通常のメソッドの引数と同じです。

# (注)ここではdefine_methodで作成されるtwoメソッドを、普通に(?)定義した場合を示しています。
# 便宜上num=2としています。ブロックによる記述も、通常のメソッドの定義方法とあまり変らないのか、
# と感じて頂ければと思います。
def two(i = nil)
  num = 2
  i ? num * i : num
end

nineについても全く同じ考え方になります。

ちなみにsample.rbに関しては、twoメソッドやnineメソッドの引数に数値以外を渡すとエラーになります(例えばtwo("2")とか)。理由については試して頂ければ分かると思います。

【訂正】
当初の記事では上記のコードを下記のように記載していましたが、下記のコードではエラーが発生します。ローカル変数やスコープに関する認識が不足していました。
コメントを頂いた@scivolaさんには重ねてお礼申し上げます!

# (注)誤ったコードです。
num = 2
def two(i = nil)
  i ? num * i : num
end

終わりに

だんだん何について書いているのか分からなくなり、また、説明が冗長で逆にわかりづらかったら申し訳ありま温泉。
個人的にはブロックパラメーター i にはdefine_methodで作成したメソッドを呼び出す際の引数が代入される。という部分が、なるほどポイントでした。
自分と同じような初学者の方に、一部分でも参考になる点があれば幸いです。
また、誤り・改良すべき点等あればご指摘いただければありがたいがーです。

oaths3
学びの中の気づきや、調べてもあまり情報が出てこない事を共有するため、少しづつ記事を書いていきたいと思っています。よろしくお願いします。
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