2. キーワード引数
2013/12/22 シナジーマーケティング(株) 鈴木 圭
[Ruby 2.0] 第2章 キーワード引数
- 2.1. キーワード引数とは
- 2.2. キーワード引数以前のアプローチ
- 2.3. メソッドに渡す最後の引数がハッシュの場合の問題
- 2.4. まとめ
2.1. キーワード引数とは
Ruby2.0 で登場したキーワード引数は、メソッドの引数にキーワード(名前)を付けることができる機能です。別の言い方をすると、メソッドを呼び出す時に「このキーワードの引数にはこの値を渡す」という書き方ができるようになる機能です。
省略可能な引数とデフォルト値
例として、以下のコードを見てください。
def log(message, level='INFO', output=STDOUT) output.puts("[#{level}] #{message}") end
これはログを出力する log メソッドを定義するコードです。引数は以下のとおりです。
- 第1引数: 出力するメッセージ
- 第2引数: ログレベル (Ex. ‘INFO’, ‘WARN’, ‘ERROR’)
- 第3引数: 出力先となる IO オブジェクト
この log メソッドは以下のように呼び出すことができます。
log('Hello', 'INFO', STDOUT)
第2引数と第3引数にはデフォルト値が設定してありますので、引数を省略することができます。
log('Hello')
第2引数は省略して第3引数だけ指定したい場合
それでは第2引数は省略して第3引数だけ指定したい場合はどうなるでしょうか。
Ruby1.9 まではそれを実現する方法は無く、第2引数にデフォルト値と同じ値を明示的に指定するしかありませんでした。
log('Hello', 'INFO', STDERR)
キーワード引数による解決
このコードを Ruby2.0 で登場したキーワード引数を使うように書き換えると以下のようになります。
def log(message, level: 'INFO', output: STDOUT) output.puts("[#{level}] #{message}") end
「キーワードA: デフォルト値A, キーワードB: デフォルト値B, …」という書き方をします。この記法はハッシュのリテラルと同じです。
log メソッドの呼び出しは次のようになります。
log('Hello', output: STDERR)
メソッドの呼び出しにおいても、キーワード引数の部分は「キーワードA: 値A, キーワードB: 値B, …」という書き方です。メソッドの引数は「ハッシュと同じ書き方」ではなく「渡しているものはハッシュそのもの」です。つまり、上記コードは次のようにハッシュが代入された変数を引数に渡しても同じ動作となります。(※このことによる注意点は「2.3. メソッドに渡す最後の引数がハッシュの場合の制約」で解説します。)
optional_arguments = {output: STDERR} log('Hello', optional_arguments)
キーワード引数を使うことで、第2引数は省略して第3引数を指定する、ということが可能となりました。「何番目の引数」と数えるのではなく、「level に渡す値」「output に渡す値」という具合にキーワードで考えることができる点が大きな違いです。引数を順番ではなくキーワードで参照することができるため、可読性も向上します。見比べてみましょう。
# キーワード引数を使わない場合 log('Hello', 'INFO') # キーワード引数を使う場合 log('Hello', level: 'INFO')
キーワード引数を使う場合の方が、メソッドを呼び出すコードから読み取ることができる情報量が多いことが分かります。
キーワードを間違えた場合
以下のようにキーワード引数のキーワードを間違えた場合はどうなるでしょうか。
log('Hello', no_such_keyword: 'INFO')
存在しないキーワードを指定すると、ArgumentError が発生します。
ArgumentError: unknown keyword: no_such_keyword
正しいキーワードかどうかは Ruby によって確認されます。
2.2. キーワード引数以前のアプローチ
キーワード引数がなかった時代には、以下のようなアプローチが使われていました。
# 省略可能な引数はハッシュで受け取る def log(message, options={}) # (1) デフォルト値の処理 options = options.dup # 呼び出し元に影響を与えないようにコピーする level = options.delete(:level) || 'INFO' # 値が指定されなかった場合は 'INFO' を使用する output = options.delete(:output) || STDOUT # 値が指定されなかった場合は STDOUT を使用する # (2) 無効な引数の処理 unless options.empty? raise ArgumentError, "無効な引数です: #{options.keys.join(',')}" end output.puts("[#{level}] #{message}") end
第2引数の options に注目してください。options は省略可能な引数を受け取るためのハッシュです。
メソッドの呼び出しは以下のようになります。
log('Hello', output: STDERR)
メソッドの呼び出し方はキーワード引数の場合と同じです。キーワード引数との違いは、引数の渡し方ではなく、メソッドの実装にあります。
まず、「# (1) デフォルト値の処理」の部分では値が省略された場合の処理を行っています。変数の level と output には指定された値、もしくはデフォルトの値が代入されます。
次に「# (2) 無効な引数の処理」の部分では、無効な引数が指定された場合に ArgumentError を raise するようにしています。(1) の処理で「options.delete(:level)」のようにハッシュからキーを削除しているので、(2) の部分でハッシュに何か含まれているとすると、それは無効なキーが指定されたことを意味します。
キーワード引数が登場した今なら、(1) と (2) の処理を記述する必要はありません。
また、キーワード引数と比較した場合のデメリットとして、省略可能な引数が options というハッシュに隠れてしまう点があります。以下のコードを見ると違いは一目瞭然です。キーワード引数を使わない場合は、メソッドの宣言部分を見ただけでは何を渡せばよいのか分かりません。
# キーワード引数を使わない場合 def log(message, options={}) ... end # キーワード引数を使う場合 def log(message, level: 'INFO', output: STDOUT) ... end
2.3. メソッドに渡す最後の引数がハッシュの場合の制約
※2013/12/22 追記:Ruby-2.0.0-p247 で必須引数とキーワード引数の優先順位が変更され、以下の問題は解消しました。
キーワード引数に関して、一つだけ注意があります。メソッド呼び出し時の最後の引数がハッシュとなる場合に制約があります。
以下のコードを見てください。
def f(x, y, z: 777) ... end
この f メソッドを以下のように呼び出すと以下のようになります。
x = 'Hello' y = {:abc => 123} f(x, y) # => unknown keyword: abc (ArgumentError)
これは、上記の y が単なるハッシュなのか、キーワード引数のためのハッシュなのか区別する方法が無いためです。
この制約を回避するには、次のように最後の引数に空のハッシュを渡します。
f(x, y, {})
※これは Ruby の不具合ではなく、キーワード引数の制約です。
2.4. まとめ
今回はキーワード引数について解説しました。キーワード引数のもつ大きな特徴をまとめると、以下のようになるでしょう。
- 引数を順番(○番目)ではなく、キーワード(名前)で指定することができる。
- メソッドの宣言部分から得られる情報量が多い(「def log(message, options={})」では何を渡して良いか分からない)。