クイズ:引数をそのまま戻すメソッドを作りなさい。
与えた引数がそのまま戻ってくるメソッドってありますよね、p
とか。そんなメソッドを作ってください。ただし引数は 1 つとは限りません。(キーワード引数やブロックは考えないことにします。)
つまり、以下のような結果が得られるメソッドです。
return_args() # => nil
return_args(1) # => 1
return_args(1, 2) # => [1, 2]
クイズ形式にしてみましたが、私自身がそんなメソッドを作った際に引っかかってしまったので戒めの記事です。無駄に長くなってしまったので、結論だけ知りたい人は#結論へどうぞ。
記事中の実行結果はすべて以下のバージョンの Ruby を用いています。
ruby 3.1.1p18 (2022-02-18 revision 53f5fc4236) [x86_64-linux]
引数転送 ...
が使えるのでは?
Ruby 2.7 で...
による引数の転送ができるようになりました。
def foo(...)
bar(...)
end
これを使えばいいのでは?
def return_args(...)
...
end
return_args.rb:2: warning: ... at EOL, should be parenthesized?
return_args.rb:3: syntax error, unexpected `end'
ダメでした。単なる...
は範囲演算子の...
と判定されてしまうのでした。
引数転送の...
にはカッコが必要らしいので、付けてみましょう。
def return_args(...)
return(...)
end
return_args.rb:2: syntax error, unexpected ')'
return(...)
やっぱりダメでした。ちなみにreturn
を入れてみましたが、入れても入れなくても結果は変わりません。...
はあくまでも引数を別メソッドに転送するための記法であり、戻り値には使えないようです。1
Splat 演算子 * で受け取る
...
はこうした場合には使えないことが分かったので、Splat 演算子で引数を受け取ることにしましょう。
def return_args(*args)
args
end
引数が 2 個以上であれば上のコードでいいのですが、引数が 0, 1 個の場合戻り値が配列になってしまってよろしくありません。
return_args() # => [] ←×
return_args(1) # => [1] ←×
return_args(1, 2) # => [1, 2] ←○
戻り値にも Splat 演算子を使えばいいのでは?
Splat 演算子を使えば、配列を展開できます。
a = [1, 2, 3]
foo *a # foo 1, 2, 3 と書いたのと同じ
a = [1]
foo *a # foo 1 と書いたのと同じ
ということは、こうなるのでは?
a = [1, 2, 3]
return *a # return 1, 2, 3 と書いたのと同じ
a = [1]
return *a # return 1 と書いたのと同じ…?
というわけで、↓ で行ける?
def return_args(*args)
*args
end
return_args.rb:2: syntax error, unexpected '\n', expecting '='
*args
む、よく分からないが構文的にダメらしい。return
付けてみたらどうだろう?
def return_args(*args)
return *args
end
エラーが起きない!よし、これで行けた!
と思いきや
return_args() # => [] ←×
return_args(1) # => [1] ←×
return_args(1, 2) # => [1, 2] ←○
あれ、*
無しの場合と変わらない…
引数の数で処理を分ける
仕方ない、引数が 1 個以下の場合は別対応しましょう。
def return_args(*args)
args.size <= 1 ? args.first : *args
end
return_args.rb:2: syntax error, unexpected *
...args.size <= 1 ? args.first : *args
あれ、また SyntaxError だ。三項演算子だとダメなのか。if
に替えよう。
def return_args(*args)
if args.size <= 1
args.first
else
*args
end
end
return_args.rb:5: syntax error, unexpected '\n', expecting '='
*args
そうだった、さっきも同じミスしていたのに… *args
だけではエラーになるのでreturn
を付けよう。
def return_args(*args)
if args.size <= 1
return args.first
else
return *args
end
end
できたー!…でも、ずいぶんとゴチャゴチャしてしまったなぁ…
先人の知恵を借りる
そういえば、p
とかpp
はどうやっているんだろう?
def pp(*objs)
# 略
objs.size <= 1 ? objs.first : objs
end
あれ、これだけでいいの?
制御構造 (Ruby 3.1 リファレンスマニュアル) #return
式が2つ以上与えられた時には、それらを要素とする配列をメソッドの戻り値とします。
なるほど、戻り値が 2 個以上あるときは結局配列になっているのか。だから引数が 2 個以上の場合にはわざわざ Splat で展開しなくても、配列そのままで戻せばいいわけね。
そうか、Splat 演算子を使うという考えにとらわれていたから三項演算子がダメだったりreturn
必須だったりしたけれど、使わなければこれだけシンプルに書けるのか。
結論
こうしましょう。
def return_args(*args)
args.size <= 1 ? args.first : args
end
return *args
で行けると思い込んでいたのでテストもしておらず、某ライブラリを 1 年近く適切に動かないまま公開してしまっていました。すみません。
-
そもそも現実的には「引数をただそのまま戻すメソッド」ではなく「引数を何か処理して、そしてそのまま戻すメソッド」を作るでしょうから、実際の引数にアクセスできない
...
はそういう意味でも使えません。(「何か処理して」の部分を他のメソッドにぶん投げれば使えますが。) ↩