Rubyの引数いろいろ

昨年のクリスマスにRuby3.0がリリースされました。8年ぶりのメジャーバージョンアップで、その目玉として静的型付けが導入されたことはご存知の方も多いのではないでしょうか。
それ以外にも様々な変更がありましたが、そのうちの一つでキーワード引数の扱いが変更になりました。引数は種類が多くてややこしいですよね。私もよくわからなくなるのでまとめてみようと思います。

引数の書き方

Rubyは関数呼び出しや、定義の時に引数のカッコを省略可能です。

def greet name
  puts "Hello, #{name}!"
end

greet "Taro" #=> "Hello, Taro!"

と書いても、

def greet(name)
  puts "Hello, #{name}!"
end

greet("Taro") #=> "Hello, Taro!"

と書いてもOKです。私はカッコをつける派ですが、付けないコードもよく見かけます。

デフォルト引数

引数に初期値を持たせることができます。これによりメソッド呼び出し時に、引数が必須ではなくなります。

def greet(name="Taro")
  puts "Hello, #{name}!"
end

greet #=> "Hello, Taro!"
greet("Jiro") #=> "Hello, Jiro!"

def greet(name)
  puts "Hello, #{name}!"
end

greet #=> ArgumentError

デフォルト引数と普通の引数の両方を渡したい場合は、引数の個数からRubyが推測をしてくれます。

def greet(name="Taro", age)
  puts "#{name} is #{age}."
end

greet(10) #=> "Taro is 10."
greet("Jiro", 10) #=> "Jiro is 10."

可変長引数

引数の個数が決まっていない場合に使用します。名前の通りですね。

def greet(*name)
  name.each do |name|
    puts "Hello, #{name}!" 
  end
end

greet("Taro", "Jiro") #=> "Hello, Taro" "Hello, Jiro"

引数の前に*を付けます。これで複数の引数を配列として受け取れます。配列なので、ループ等で処理してあげます。

可変長引数と、普通の引数の両方を渡したい場合は、デフォルト引数の場合と同様、引数の数からRubyが推測してくれます。

def greet(*name, msg)
  name.each do |name|
    puts "#{msg}, #{name}!"
  end
end

greet("Taro", "Jiro", "Hello") #=> "Hello, Taro!" "Hello, Jiro!"

オプション引数

可変長引数に類似の引数です。引数にキーとバリューの組み合わせを指定したい場合に使用します。引数は可変長です。

def greet(**args)
  args.each do |arg|
    puts "#{arg[0].capitalize} is #{arg[1]}."
  end
end

greet(name: "Taro", age: 10) #=> "Name is Taro." "Age is 10."

引数の前に**を付けます。これで複数のキーとバリューの組み合わせをハッシュとして受け取れます。指定するキーはなんでも良いので使い道は限定されると思います。あらかじめキーだけは指定しておくキーワード引数の方が使用頻度は高いと思います。

キーワード引数

引数にキーとバリューの組み合わせを指定します。引数が複数あると、何をどの順に渡しているのか分かりにくくなります。そういう場合はキーワード引数を使います。呼び出す時は、キーとバリューをセットで渡します。オプション引数との違いは、キーはあらかじめ指定しておく必要があるということです。

def greet(name:, age:)
  puts "#{name} is #{age}."
end

greet(name: "Taro", age: 10) #=> "Taro is 10."

どの引数がどういう意味を持つのかが一目瞭然だと思います。

キーワード引数はデフォルト引数を渡すこともできます。

def greet(name: "Taro", age: 10)
  puts "#{name} is #{age}."
end

greet #=> "Taro is 10."
greet(name: "Jiro") #=> "Jiro is 10."

キーワード引数はRuby2.0から導入された機能で、それ以前はハッシュを使ってキーワード引数を擬似的に実現していました。

def greet(options = {})
  name = options[:name]
  age = options[:age]

  puts "#{name} is #{age}."
end

greet(name: "Taro", age: 10) #=> "Taro is 10."

引数にキーとバリューの組み合わせを指定する場合、明示的にハッシュで書いても、{}を省略した形で書いても暗黙的に双方向に変換されます。

# キーワード引数の場合
def greet(name: "Taro", age: 10)
  puts "#{name} is #{age}."
end

profile = {name: "Jiro", age: 20}
greet(profile) #=> "Jiro is 20."
greet(name: "Jiro", age: 20) #=> "Jiro is 20."


# 普通の引数でハッシュを受け取る場合
def greet(options = {})
  name = options[:name]
  age = options[:age]

  puts "#{name} is #{age}."
end

greet(profile) #=> "Jiro is 20."
greet(name: "Jiro", age: 20) #=> "Jiro is 20."

しかしRuby3.0からは、この仕様が変更になっています。キーワード引数の扱いが独立し、キーワード引数に{}をつけた場合に動かなくなりました。(普通の引数でハッシュを受け取る場合は互換性が維持されています。)従って明示的に、ハッシュを展開する必要があります。**をつければOKです。

def greet(name: "Taro", age: 10)
  puts "#{name} is #{age}."
end

profile = {name: "Jiro", age: 20}
greet(profile) #=> Ruby3.0からエラー
greet(**profile) #=> "Jiro is 20."

***

*Splat Operator)は配列を展開する演算子**Double Splat Operator)はハッシュを展開する演算子です。 Javascriptで言うところの...Spread Operator)のような感じだと思います。

array = [1, 2, 3]
hash = {a: 1, b: 2}

[a, b, array, c] #=> [a, b, [1, 2, 3], c]
[a, b, *array, c] #=> [a, b, 1, 2, 3, c]

{A: 10, B: 11, hash, C: 12} #=> SyntaxError
{A: 10, B: 11, **hash, C: 12} #=> {A: 10, B: 11, a: 1, b: 2, C: 12}

***を使うと配列やハッシュを展開できるので、逆も可能です。これを利用したのが可変長引数、オプション引数です。 メソッド定義の際に*nameのように引数を指定すると、呼び出すときに引数が配列に変換されるという仕組みです。

ブロック引数

引数にブロックを指定できます。定義する際は&を引数の前に書きます。メソッド内ではyieldでブロックを呼び出します。

def greet(&block)
  yield
end

greet { puts "hello" } #=> "hello"

ブロック引数にロックを渡したい場合は、呼び出しの際も&を書いてやればOKです。

def greet(&block)
  puts block.yield("hello")
end

proc_obj = proc { |str| str.upcase }
greet(&proc_ojb)

定義時に引数にブロックを指定していないメソッドにもブロックは渡すことができます。ただ、注意点としては呼び出し方はyieldのみ限定されます。callで呼び出すことはできません。

def greet
  if block_given?
    yield
  else
    puts "No block"
  end
end

proc_obj = -> do
  File.write("hoge.txt", "Hello, world")
  File.read("hoge.txt")
end

greet(&proc_obj) #=> "Hello, world"
greet #=> "No block"

if block_given?で引数にブロックが渡ってきているか条件分岐が可能です。

ブロックの呼び出し方いろいろ

block.call
block.yeild
yeild

procはオブジェクトの作り方も、呼び出し方も何パターンもあるのでまた別に書きたいと思います。

参考
https://techlife.cookpad.com/entry/2020/12/25/155741
https://qiita.com/jnchito/items/74e0930c54df90f9704c
https://qiita.com/rtoya/items/33617078501776fdcad7
https://ryotatake.hatenablog.com/entry/2018/11/20/splat_operator