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

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

こんにちは、hachi8833です。「Rubyのパラメータと引数の対応付けを理解する(前編)」に続き、後編をお送りいたします。

申し遅れましたが、動作はRuby 2.4.1で確認しています。

元記事について

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

元記事の後半の部分は検証しきれなかったため省略いたしました。

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

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

  • 前編
    • メソッド定義のパラメータの解説と、パラメータの正しい順序
  • 後編
    • メソッド呼び出しの引数と、引数の順序

用語「パラメータ」「引数」について(再録)

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

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

メソッド呼び出しの引数の種類

前半戦でパラメータについて解説しましたので、後半戦は引数を中心とします。
Rubyの引数とパラメータがどう対応付けられているかを理解するのはかなり大変です。

メソッド呼び出し側で渡せる引数の種類は、元記事では以下の7つに分類されています。

種別
通常 v
"hello"
[1, 2, 3]
キーワード b: v
:b => v
ハッシュ v1 => v2
splat *v
ダブルsplat **v
ブロック変換 &v
ブロック { ... }または do ... end

上の引数をそのまま使ったメソッド呼び出し例を次に示します。

foo 1, *array, 2, c: 3, "d" => 4, **hash

演算子の優先順位に注意すれば、vv1v2にはほぼどんな式でも代入できます。

ところで次をご覧ください。無効な組み合わせももちろんありますが、パラメータと引数の種類の組み合わせの多さはやばいです。

そしてsplatやダブルsplatがパラメータ側だけでなく引数側にもある点にご注意ください(ただし機能は異なります)。このあたりから不安になってきますね。ともあれ、各引数について解説します。

1. 通常の引数

ここで言う通常の引数には、以下を含めるものとします。いずれも引数の個数としては1個になります。

  • 変数としての引数(vなど)
  • 文字列リテラル("hello"など)
  • 配列リテラル([1, 2, 3]など)

パラメータ側を必須パラメータ1つに限った場合(ここではa)、通常の引数は以下のようにそのまま渡されます。

def foo(a)
  puts "a: #{a}"
end

v= "hello"
foo v           #=> a: hello                      変数になっている引数はすべてそのまま渡される
foo "hello"     #=> a: hello                      文字列リテラルはそのまま渡される
foo %w(foo bar baz)
#=> a: ["foo", "bar", "baz"]                      配列リテラルはそのまま渡される 

引数に置かれた変数は、中身が何であっても1個の引数として1個のパラメータに渡されます。
引数が文字列リテラルや配列リテラルの場合も、パラメータが1個ならひとかたまりとして渡されます。

2. キーワード引数

キーワード引数は、以下のいずれかの形式で書けます。キーワードのパラメータ名にはシンボルだけが使えます。

def foo(name: "bar", addr:)
  puts "name: #{name}"
  puts "addr: #{addr}"
end

foo addr: "tokyo", name: "baz"
#=> name: baz
#=> addr: tokyo
foo :addr => "tokyo", :name=> "baz"
#=> name: baz
#=> addr: tokyo

キーワード引数同士であれば、順序は不問です。

3. ハッシュ引数

ハッシュ引数(「ハッシュ形式の引数」)は、形式的には2.のキーワード引数ときわめて似ています。要するにRubyの一般的なハッシュなので、keyがシンボルに限定されない点がキーワード引数と異なります。

  • b: v
  • :b => v
  • b => v -- keyが変数

パラメータ側ではオプション引数やダブルsplatで受けるのが一般的です。

def foo(opts = {})
  puts "opts: #{opts}"
end
def bar(**opts)
  puts "opts: #{opts}"
end

foo addr: "tokyo", name:"baz", hello: :world  #=> opts: {:addr=>"tokyo", :name=>"baz", :hello=>:world}
bar addr: "tokyo", name:"baz", hello: :world  #=> opts: {:addr=>"tokyo", :name=>"baz", :hello=>:world}

引数全体が丸かっこ( )で囲まれていれば、ハッシュ引数を波かっこ{ }で囲んでハッシュリテラルとして書くこともできます。

def foo(opts1 = {}, opts2 = {})
  puts "opts1: #{opts1}"
  puts "opts2: #{opts2}"
end

foo({addr: "tokyo", name: "baz"}, {mail: "ex@example.com", server: "mail.example.com"})
#=> opts1: {:addr=>"tokyo", :name=>"baz"}
#=> opts2: {:mail=>"ex@example.com", :server=>"mail.example.com"}

参考: 単独の必須パラメータはハッシュ引数を取れる

パラメータ側に(キーワードパラメータではない)単なる必須パラメータが1つしかない場合、ハッシュ引数は個数にかかわらずハッシュの形にまとめて1個の必須パラメータに渡されます。

ハッシュ引数をこんなふうに必須パラメータ1つで雑に受けるのは行儀がよくないですね。

def foo(a)
  puts "a: #{a}"
end

foo addr: "tokyo", name: "baz", hello: :world 
#=> a: {:addr=>"tokyo", :name=>"baz", :hello=>:world}

また、必須パラメータが2つ以上の場合はさすがにエラーになります。「given 1」となっているので、必須パラメータ1つに対してキーワード引数の集まり全体が1つの引数とみなされていることがわかります。

def foo(a, b)
  puts "a: #{a}"
  puts "b: #{b}"
end
foo addr: "tokyo", name: "baz"
#=> ArgumentError: wrong number of arguments (given 1, expected 2)

参考: ハッシュリテラルは丸かっこなしでは引数に書けない

ハッシュリテラルは、{addr: "tokyo", name:"baz", hello: :world}のようにkey-valueペアを波かっこで{ }囲んだ形式です。
Rubyでは、引数全体が丸かっこ( )で囲まれていない場合、ハッシュをリテラルで生書きすることはできません。この形にするとブロックと似てしまうのがよくないからだと思われます。

def foo(a)
  puts "a: #{a}"
end

foo {:addr=>"tokyo", :name=>"baz", :hello=>:world}
#=> SyntaxError: unexpected ',', expecting end-of-input

4. splat引数

*を引数の前に追加したものをsplat引数と呼んでいます。splat引数では、引数の中に入っている配列を、パラメータに渡す前に展開します。

たとえばfoo(1, *[2, 3], 3)foo(1, 2, 3, 4)と同等です

def foo(a, b, c)
  puts "a: #{a}"
  puts "b: #{b}"
  puts "c: #{c}"
end

v = %w(hoge huga hugo)
foo *v
#=> a: hoge
#=> b: huga
#=> c: hugo

splat引数では、引数に配列(または#to_aをサポートするオブジェクト)が入っていること、メソッド定義側にも配列の要素と同数のパラメータがあることを前提にするのが普通です。

なお、元記事に準じてsplatパラメータとsplat引数という用語にしていますが、splatパラメータは「個数が可変長の引数をまとめて1つのパラメータで受ける」のに対し、splat引数は「配列を要素に分解してから別々のパラメータに渡す」ものなので、両者は同じではありません。操作は互いに逆ですし、前者は引数の種類を問わないのに対し後者は配列(または#to_aをサポートするオブジェクト)を対象とすることが前提になっています。

5. ダブルsplat引数

**が付いたダブルsplat引数は、splat変数の対象がハッシュになったような動作であり、#to_hashメソッドをサポートするオブジェクトを暗黙でHashオブジェクトに変換してからパラメータに渡します。

たとえば、foo(a: 1, **{b: 2, c: 3}, e: 4)はfoo(a: 1, b: 2, c: 3, d: 4)と同等です。

def foo(mail, opts = {})
  puts "mail: #{mail}"
  puts "opts: #{opts}"
end

foo("ex@example.com", **{addr: "tokyo", name: "baz"})
#=> mail: ex@example.com
#=> opts: {:addr=>"tokyo", :name=>"baz"}

なお、#to_hメソッドは#to_hashメソッドと異なり、明示的に使わないと実行されない変換です。

6. ブロック変換

引数の前に&を付けると、引数がブロックに変換されてからメソッドに渡されます。この引数には、Procオブジェクトか、#to_procで暗黙に変換可能なオブジェクトを置く必要があります。

def foo
  yield
end

v = ->{ puts "hi" }
foo &v
#=> hi

変換されたブロックは、メソッド定義側から見れば7.のブロックと同じになります。したがって、メソッド定義側で暗黙のブロックとしてyieldで呼び出すことも、&付きのブロックパラメータで受け取ってもっと自由に使うこともできます。

7. ブロック

Rubyのブロックは以下のいずれかの形式で書きます。

  • { }: 1行で収まるコードに使う
  • do...end など: 複数行にわたるコードに使う

ブロックは引数リストの最後尾に置く必要があります。

def foo
  yield
end

foo { puts "hello" }
foo do
  puts "hello"
  puts "goodbye"
end

引数の順序

さまざまな種類の引数が使われている場合、順序に注意が必要です。splatやダブルsplatは引数側での展開なので表には含めていません。

種別
1.通常(デフォルト値ありを含む) v
"hello"
[1, 2, 3]
2.可変長の引数(ハッシュを含む) v1, v2, ...
v: 1, v: 2
3.キーワード b: v
:b => v
4.ブロック変換またはブロック(一方のみ) &v{ ... }do ... end
  • ブロック変換やブロックは、どちらか1つだけを引数リストの最後に置かなければなりません。
  • 通常の引数やsplat引数は、キーワード引数やダブルsplat引数よりも前に置かなければなりません。

キーワード引数が順序不問なのはあくまでキーワード引数同士の間に限られますので、他の種類の引数と順序が入り交じると期待どおりに動かないことがあります。

def foo(req, *spl, mail:)
  puts "req: #{req}"
  puts "spl: #{spl}"
  puts "mail: #{mail}"
end

# 通常の引数、
foo("required", {name: "bar", addr: "baz"}, mail: "ex@example.com")
#=> req: required
#=> spl: [{:name=>"bar", :addr=>"baz"}]
#=> mail: ex@example.com

# キーワード引数が通常の引数より先にあるとエラーになる
foo(mail: "ex@example.com", "required", {name: "bar", addr: "baz"})
#=> SyntaxError: unexpected ')', expecting end-of-input

キーワードパラメータとハッシュパラメータが両方ある場合や、ハッシュパラメータが複数ある場合は、必要に応じてハッシュ引数を{ }で囲みましょう。

引数の種類の順序も、原則として前編の末尾に書いたパラメータの順序に準拠するのが無難です。

関連記事


CONTACT

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