Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rubyのパラメータと引数の対応付けを理解する(前編)

こんにちは、hachi8833です。今回は英文技術ブログ記事「Ruby Methods, Procs and Blocks」を参考に、Rubyのパラメータと引数についてまとめました。

分量が元記事よりもかなり増えたので、パラメータの話を前編、引数の話を後編として分割いたします。

元記事について

原著者の許諾を得て追記・再構成のうえ公開いたします。

元記事のコメントや訂正文もチェックしました。

Rubyのパラメータと引数の対応付けを理解する(前編)

本記事では以下のトピックについて扱います。

  • 前編
    • メソッド定義のパラメータの解説と、パラメータの正しい順序
  • 後編
    • メソッドが呼び出されたときに引数がパラメータにどのように代入されるか
    • procとブロックの関連
    • lambdaとprocの違い
    • 2種類のブロック{ ... }do ... end?の違い

用語「パラメータ」「引数」について

本記事では混乱を避けるため、元記事に基いてパラメータと引数という用語を以下のように使い分けます。

パラメータ(parameter)
メソッド定義側で書くものを指す
(厳密には「メソッドパラメータ」なのでブロック内のパラメータは含めない)
引数(argument)
メソッド呼び出し側で渡すものを指す

各パラメータの種類

元記事では、Rubyのメソッド定義で使えるパラメータを以下の8つに分類しています。

種別
必須 a
オプション b = 2
配列分解 (c, *d)
splat *args
post-required f
キーワード g:, h: 7
ダブルsplat **kwargs
ブロック &blk

配列分解(array decomposition)とpost-requiredは、元記事著者独自の呼び方のようです。

上のパラメータをすべて使った例は以下のようになります。

def foo a, b = 2, *c, d, e:, f: 7, **g, &blk; end

パラメータの説明

注意: 元記事で使われているパラメータの種類は、パラメータを修飾する記号の書式だけでは決まりません。元記事におけるパラメータの種類は、「パラメータを修飾する記号」、「パラメータが置かれる位置(順序)」、「パラメータにどんな引数が渡されるか」を総合したものです。

  • たとえば「記号が何もついていないから必須パラメータ」とは限らず、パラメータの配置に応じてpost-requiredパラメータと呼んでいます。
  • 3.の配列分解はパラメータの書式ではなくパラメータの動作と理解するのがよいでしょう。

1. 必須

必須パラメータは、呼び出し側で引数を与えないとエラーになるパラメータです。つまりこの「必須」は「呼び出し側で引数を省略できない」という意味になります。もっとも普通のパラメータと言ってよいでしょう。

以下は、必須パラメータに引数を渡さなかった場合のエラー表示です。

def foo(a)        # aが必須パラメータ(普通のパラメータ)
  puts "a: #{a}"
end
foo     #=> エラー

2. オプション

パラメータにデフォルト値を与えたものはオプションのパラメータになります。オプションなので、引数を渡さない場合はデフォルト値が使われ、エラーになりません。

以下は、オプションパラメータに引数を渡さなかった場合の結果です。

def foo(a = 1)  # a = 1がオプションパラメータ(デフォルト値を持つ)
  puts "a: #{a}"
end
foo     #=> a: 1

特に、opts = {}のような形式のオプションパラメータにすると、key-valueペアをいくつでもまとめて受けることができます。key-valueペアがひとつもない場合でもエラーになりません。

def foo(a, b, opts = {})  # opt = {}でkey-valueペアを受けられる
  puts "a: #{a}"
  puts "b: #{b}"
  puts "opts: #{opts}"
end

foo("bar", "baz", { hoge: 1, huga: 2, hoga: 3 })
#=> a: bar
#=> b: baz
#=> opts: {:hoge=>1, :huga=>2, :hoga=>3}

opts = {}形式のオプションパラメータの注意点については以下もご覧ください。

3. 配列分解

配列分解は、入れ子になった配列をリテラルで(=変数に入れずに)渡された場合に配列を部分に切り分けて受け取る、特殊な動作です。

分解のパターンは、パラメータ側のsplat(後述)の配置や丸かっこ( )によって複雑に変化します。引数に[1, 2], 3を与えた場合の例を以下に示します。

(*a)          # a = [[1, 2], 3]
(a, b)        # a = [1, 2], b = 3
(a, *b)       # a = [1, 2], b = [3]
(a, b, *c)    # a = [1, 2], b = 3,   c = []
((a, b), c)   # a = 1,      b = 2,   c = 3
((a, *b), c)  # a = 1,      b = [2], c = 3

かなり複雑ですね。パズルみたいで楽しそうですが、パラメータの数が増えると、入れ子の配列リテラルの分解を正確に理解して引数で渡すのは至難の業です。引数に配列リテラルをうかつに書くと落とし穴にハマりそうです。

少なくとも、気が付かずに配列分解を引き起こすコードや、配列分解を前提としたコードを書くのは避けるのがよいでしょう。

上を実行可能なRubyコードにしてみました。なお、コード中のセミコロン;はpryのエコーバックを抑制するために追加しました。

def method1(*a)
  puts "a: #{a}"
end;
def method2(a, b)
  puts "a: #{a}"
  puts "b: #{b}"
end;
def method3(a, *b)
  puts "a: #{a}"
  puts "b: #{b}"
end;
def method4(a, b, *c)
  puts "a: #{a}"
  puts "b: #{b}"
  puts "c: #{c}"
end;
def method5((a, b), c)
  puts "a: #{a}"
  puts "b: #{b}"
  puts "c: #{c}"
end;
def method6((a, *b), c)
  puts "a: #{a}"
  puts "b: #{b}"
  puts "c: #{c}"
end;
method1 [1, 2], 3;      # a: [[1, 2], 3]
method2 [1, 2], 3;      # a: [1, 2], b: 3
method3 [1, 2], 3;      # a: [1, 2], b: [3]
method4 [1, 2], 3;      # a: [1, 2], b: 3, c: []
method5 [1, 2], 3;      # a: 1, b: 2, c: 3
method6 [1, 2], 3;      # a: 1, b: [2], c: 3

4. splat

パラメータ文字の前に*を追加すると、長さが不定の引数リスト(可変長引数リスト)を受けられるようになります。これをsplatパラメータと呼んでいます。上の配列分解の例にも、splatパラメータが含まれているものがあります。

splatパラメータは、さまざまな種類の引数をいくつ渡してもすべて受け取れます。

def foo(*a)       # *aがsplatパラメータ
  puts "a: #{a}"
end

foo :one, "two", [:three, "four"]
#=> a: [:one, "two", [:three, "four"]]

5. post-required

splatパラメータの直後に置いた必須パラメータは、post-requiredパラメータとして動作します(本記事では英ママで表記します)。
この場合ちょうどsplatパラメータのストッパーのような働きをし、splatパラメータに渡される引数リストの末尾の引数を受け取ります。

以下の例では、bがpost-requiredパラメータとして動作します。

def foo(*a, b)    # bがpost-requiredパラメータ
  puts "a: #{a}"
  puts "b: #{b}"
end

foo :one, "two", {key1: "value"}, [:three, "four"] 
#=> a: [:one, "two", {:key1=>"value"}]
#=> b: [:three, "four"]

この動作からわかるように、post-requiredパラメータがあると、直前のsplatパラメータに吸い込まれる引数が1つ少なくなります

気が付かずにpost-requiredパラメータを形成したり、post-requiredパラメータを前提としたコードを書くのは、これまた避けるのがよさそうです。

6. キーワード

パラメータ名にRubyのシンボルの書式「キーワード:」を使うと、キーワード付きパラメータになります(キーワード付き引数と呼ばれることもよくあります)。

def foo(name)          # ただの必須パラメータ
  puts "name: #{name}"
end

def foo(name:)         # キーワード
  puts "name: #{name}"
end

def foo(name: "bar")   # デフォルト値付きキーワード
  puts "name: #{name}"
end

キーワード付きパラメータの最大のメリットは、メソッド呼び出し側でのキーワード付き引数の順序が不問になることです。メソッドを呼び出す側にとっては順序を気にしなくてよくなるので大変使いやすくなります。

  • キーワード付きパラメータにデフォルト値を与えないと引数が必須になります
    • 引数がない場合や、またはキーワードが示されていない場合にエラーになります
  • デフォルト値を与えれば引数がなくてもエラーになりません

特にデフォルト値付きキーワードは、メソッド呼び出し側で{key: "value"}のようなハッシュでのオプション引数渡しと同じ感覚で、しかも順序不問でkey-valueペアのオプションを渡せるので便利です。

def foo(name: "bar", addr: "tokyo", tel: "03-9999-9999")
  puts "name: #{name}"
  puts "addr: #{addr}"
  puts "tel:  #{tel}"
end

foo(tel: "03-0000-0000", addr: "shibuya", name: "baz") # 呼び出し側で順序を気にしなくてよい
#=> name: baz
#=> addr: shibuya
#=> tel:  03-0000-0000

参考

7. ダブルsplat

パラメータ名の前に**を付けるとダブルsplatパラメータになります。splatパラメータと似ていますが、可変長のキーワード引数リストを受け取る前提である点が異なります。ダブルsplatは、上述のopts = {}形式のオプションパラメータと同様の動作です。

def foo(**profile)            # ダブルsplat
  puts "profile: #{profile}"
end

foo(tel: "03-0000-0000", addr: "shibuya", name: "baz")
#=> profile: {:tel=>"03-0000-0000", :addr=>"shibuya", :name=>"baz"}

なお、可変長のキーワード引数リストをダブルでないsplatパラメータで受けると、配列の中にハッシュが保存されるので取り出しに一手間余計にかかります。

def foo(*profile)              # ただのsplat
  puts "profile: #{profile}"
end

foo(tel: "03-0000-0000", addr: "shibuya", name: "baz")
#=> profile: [{:tel=>"03-0000-0000", :addr=>"shibuya", :name=>"baz"}]   <=配列の中にハッシュがある

8. ブロック

最後のブロックは、他のパラメータと異なる点があります。

  • メソッド定義でyieldでブロックを実行する場合は、パラメータに何も書かなくても、呼び出し側で引数リストの最後にブロックを置くことで渡せます(以下: 暗黙のブロックパラメータ)。
  • 逆に、パラメータの末尾に&付きのブロックパラメータを明示的に置いてブロックを受けることもできます(以下: 明示的なブロックパラメータ)。
# 暗黙
def foo              # 普通のパラメータも、ブロックを受けるパラメータもない
  yield
end

foo { puts "hello" } #=> hello

# 明示
def foo &blk         # &blkはブロック専用のパラメータ
  blk.call
end

foo { puts "hello" } #=> hello

暗黙のブロックパラメータで受けたブロックには名前がありません。

明示的なブロックパラメータで受けたブロックには名前があるので、他のメソッドにブロックを渡したりできるようになります。後編の「ブロックとproc」を参照してください。

参考: #parametersメソッド

Method#parametersメソッドを使うと、メソッドのパラメータリストを取得できます。取得したリストには、上で説明したパラメータの種別も含まれます。先ほどのfooメソッドに対してこのメソッドを実行すると以下の結果が得られます。

method(:foo).parameters
# [[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:keyreq, :e], [:key, :f], [:keyrest, :g], [:block, :blk]]

配列分解パラメータだけは単に[:req]と表示されます。配列分解は特殊な動作なので、一意の表現形式がないのだと思います。

パラメータの順序

以上を踏まえて、いよいよメソッド定義でのパラメータの順序について述べます。

上述のパラメータをメソッド定義で好きな順序で使うことはできません。以下の順序を守って記述する必要があります(冒頭の表とパラメータの説明もこの順序に沿っています)。

1:必須パラメータ
a
2:デフォルト値付きオプションパラメータ
b = 1
3:splatパラメータ(1つまで)
*c
4:post-requiredパラメータ
*c, dの「d」の方
5:キーワードパラメータ
e:e: 1
6:ダブルsplatパラメータか
オプションハッシュパラメータ
1つまで)
**ff = {}
7:ブロックパラメータ(1つまで)
{ }do ... endなど

避けたいパラメータ

以下はなるべく避けることをおすすめします。

  • 必須パラメータとオプションパラメータの順序を混在させる
  • post-requiredパラメータの利用
  • オプションパラメータとsplatパラメータの同時利用
  • 配列分解を前提とした引数渡し

パラメータにデフォルト値を与えたいのであれば、オプションパラメータではなくキーワードパラメータを使いましょう。

補足: Rubyのキーワードパラメータ

上述のとおり、Rubyのキーワードパラメータを使うと、呼び出し側でキーワード引数の順序が不問になります。このおかげで、デフォルト値付きのオプションパラメータを、ハッシュでのオプション渡しと同じように(かつもう少し行儀よく)扱うことができ、Rubyの使い勝手のよさにつながっています。

# キーワード引数方式
def foo(name: "bar", addr: "tokyo", tel: "03-9999-9999")
  puts "name: #{name}"
  puts "addr: #{addr}"
  puts "tel:  #{tel}"
end;

# オプションハッシュ方式
def bar(opts = {})
  puts "name: #{opts[:name]}"
  puts "addr: #{opts[:addr]}"
  puts "tel:  #{opts[:tel]}"
end;

# どちらも呼び出し方は同じ、かつ順序不問
foo(tel: "03-0000-0000", addr: "shibuya", name: "baz");
bar(tel: "03-0000-0000", addr: "shibuya", name: "baz");
#=> name: baz
#=> addr: shibuya
#=> tel:  03-0000-0000

メソッド定義にないkey-valueペアを引数として与えると、キーワードパラメータの場合はエラーになります。こういうところが行儀よいですね。
オプションパラメータの場合はすべてパラメータに渡されるのでエラーにはなりません。

# キーワードパラメータにないキーワード引数を与えるとエラー
foo(tel: "03-0000-0000", addr: "shibuya", name: "baz", email: "ex@ex.com");
#=> ArgumentError: unknown keyword: email

# オプションパラメータは何でも飲み込む
bar(tel: "03-0000-0000", addr: "shibuya", name: "baz", email: "ex@ex.com");
#=> name: baz
#=> addr: shibuya
#=> tel:  03-0000-0000

なお、Objective-Cのキーワード引数は定義と同じ順序で渡さないといけないのだそうです。

また、Javaでは「メソッド名」「引数の数」「引数の型」をシグネチャと呼び、(引数の順序も含めて)シグネチャが1つでも異なっていれば同じクラス内でも異なるメソッドとみなされるのだそうです。同じ名前で複数のメソッドを定義できるようにするためのメカニズムであるようです。

同じクラスで同名メソッドを複数定義できないRubyとはかなり考え方が違いますね。

今回はここまでとします。後編にご期待ください。

関連記事


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。