104
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rubyのキーワード引数はシンボルっぽく定義するけど、シンボルそのものではない、という話

Last updated at Posted at 2019-12-30

はじめに

拙著「プロを目指す人のためのRuby入門(通称チェリー本)」の読者さんの中で、ときどきキーワード引数について困惑している人を見かけるので、この記事でキーワード引数に関する説明を補足しておきます。

Ruby初心者さんの疑問:キーワード引数はシンボルじゃないの?

Ruby初心者さんの中には、キーワード引数をシンボルとして参照できないことに疑問を持つ方がおられるようです。

たとえばこんな感じです。(「プロを目指す人のためのRuby入門」5.4.3項のコード例を一部改変)

def buy_burger(menu, drink: true, potato: true)
  # なんで :drink じゃないの?
  if drink
    # ...
  end
  # なんで :potato じゃないの?
  if potato
    # ...
  end
end

なるほど、そう言われてみると、メソッド定義のキーワード引数はたしかにキーがシンボルのハッシュっぽい書き方になってますね・・・。

# drink: true, potato: true の部分だけ見ると、
# たしかにキーがシンボルのハッシュっぽい
def buy_burger(menu, drink: true, potato: true)

# 実際、こう書けばキーがシンボルのハッシュになる
hash = { drink: true, potato: true }
hash[:drink]  #=> true
hash[:potato] #=> true

でも、これは記法が似ているだけであって、シンボルではないんです。
あくまでメソッドの引数(仮引数)なんです。

ですので、上のメソッドのmenuと同じように、メソッド内ではdrinkpotatoのように参照します。

もし、:drink:potatoのように書いてしまうと、これはシンボルオブジェクト(シンボルリテラル)を書いたことになります。

def buy_burger(menu, drink: true, potato: true)
  # キーワード引数もメソッドの引数なので、menuと同様にdrinkと書く
  if drink
    # ...
  end
  
  # :drinkと書いた場合はメソッドの引数とは無関係な、ただのシンボルオブジェクトになってしまう
  if :drink
    # ...
  end
  
  # ...
end

とりあえず、この点については「キーワード引数はシンボルではないんです。定義するときの書き方はたしかに似てるけど、そういうもんなんです」としか説明のしようがありません。

なぜキーワード引数をシンボルっぽく書くのか?その歴史的経緯

とはいえ、キーワード引数をシンボルっぽく書くのは、おそらく歴史的な経緯があると考えています。

プロを目指す人のためのRuby入門」の5.6.3項にも書いたように、キーワード引数が導入される前(つまりRuby 1.9以前)はハッシュを使って擬似的にキーワード引数を実現していました。

# ハッシュを引数として受け取り、疑似キーワード引数を実現する
# (キーワード引数が導入される前はこう書くしかなかった)
def buy_burger(menu, options = {})
  # 引数のハッシュから目的の値をローカル変数に格納する
  drink = options[:drink]
  potato = options[:potato]
  
  # 引数から取り出した値を使って処理を書く
  if drink
    # ...
  end
  
  # ...
end

# このメソッド呼び出しではキーワード引数ではなく、
# { drink: true, potato: true } というハッシュオブジェクトを
# 引数として渡している
buy_burger('cheese', drink: true, potato: true)

このようにRubyの世界では昔からハッシュとキーワード引数が近しい関係にあったため、キーワード引数の定義の仕方もハッシュの記法に似たものになったのだろう、と僕は考えています。

# 昔はみんなハッシュで書いてたんだから・・・
def buy_burger(menu, options = {})
  # ...
end

# キーワード引数もハッシュっぽい記法になっていると自然(?)
def buy_burger(menu, drink: true, potato: true)
  # キーワード引数にすると、このローカル変数が引数に置きかわるイメージ
  # drink = options[:drink]
  # potato = options[:potato]
  
  if drink
    # ...
  end
  
  # ...
end

# ちなみに、呼び出し方はどちらも変わりません
buy_burger('cheese', drink: true, potato: true)

ただ、この歴史的経緯を知っている人にとっては「うん、自然だね」と思えるものの、そうじゃない人にとっては「えっ、なんでシンボルじゃないの!?」とビックリする原因になるのかもしれませんね。

2020.5.7 追記:呼び出し側はシンボルなの?シンボルじゃないの?

この記事ではあくまで「メソッドを定義する側」に議論をフォーカスしています。

しかし、「シンボルそのものではない」ということを強調しすぎたがゆえに、メソッド呼び出し側の話と混同されている方も中にはおられるようです。

# drink: や potato: はシンボルなの?シンボルじゃないの??
buy_burger('cheese', drink: true, potato: true)

ややこしいですが、メソッド呼び出し側に登場する引数については「シンボルだ」と言ってもよいと思います。
なぜなら、メソッド定義側と異なり、呼び出し側は=>を使って呼び出すこともできるからです。
よって、シンボルそのもの(シンボルのリテラル)を書いていることになります。

# 呼び出す側はどっちの記法でも呼び出せる
buy_burger('cheese', drink: true, potato: true)
buy_burger('cheese', :drink => true, :potato => true)

また、シンボルを変数に格納しても呼び出せます。
このことからも呼び出し側は「シンボルを渡している」と言えると思います。

# 変数経由で呼び出すこともできる
key_1 = :drink
key_2 = :potato
buy_burger('cheese', key_1 => true, key_2 => true)

とはいえ、「呼び出す側はシンボルとして引数を渡すが、受け取る側はシンボルでない普通のメソッド引数として値を受け取る」というのは、言われてみると不自然な気もしますね。
僕はこれまで「これはこういうものだ」と考えていたので、あまり疑問に思いませんでしたが、「そう言われてみると、なるほどな〜」と思いました。

まとめ

というわけで、この記事では「Rubyのキーワード引数はシンボルっぽく定義するけど、シンボルそのものではない」という話を説明しました。

プロを目指す人のためのRuby入門」の改訂版を出す機会があれば、この内容も追加しておこうと思います。
それまではこの記事を参考にしてもらえると幸いです。

あわせて読みたい

Ruby 2.0でキーワード引数が導入されたときに書かれた記事です。
上で説明した「歴史的経緯」をより詳しく理解することができます。

一方で、この「歴史的経緯」が原因で、Ruby本体のややこしい不具合の原因になってしまったという問題もあります。
そのため、Ruby 2.7以降ではキーワード引数の扱いが変更されています。
詳しくはこちらの記事をご覧ください。

104
62
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
104
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?