16
5

【トリビア】Rubyで users .map(&:name) の形式で呼び出せるのはpublicメソッドだけ

Last updated at Posted at 2024-03-31

はじめに

たとえばUserクラスにnameというprotectedメソッドがあったとします。
(protectedメソッドの登場頻度はかなり少ないと思いますが)

class User
  protected
  
  def name
    'Alice'
  emd
end

さらに、fetch_namesというpublicメソッドも定義されていたとします。

class User
  def fetch_names(users)
    users.map { |u| u.name }
  end
  
  protected
  
  def name
    'Alice'
  end
end

ではfetch_namesメソッドを呼び出してみましょう。

users = [User.new, User.new, User.new]
users[0].fetch_names(users)
#=> ["Alice", "Alice", "Alice"]

はい、問題なく呼び出せました。
(注:Rubyのprotectedメソッドは同じクラス、またはサブクラスから呼び出せるメソッドです)

しかし、リファクタリングすると……?

Ruby経験者であれば、fetch_namesの実装を見て「もっと短く書きたい!」と思うはずです。
そう、経験者ならRubyのイディオム的にusers.map(&:name)のように書くことが多いはずです。
やってみましょう。

class User
  def fetch_names(users)
    users.map(&:name) # リファクタリング!?
  end
  
  protected
  
  def name
    'Alice'
  end
end

users = [User.new, User.new, User.new]
users[0].fetch_names(users)
#=> protected method `name' called for an instance of User (NoMethodError)

おっと、リファクタリングしたつもりが NoMethodError になってしまいました!

users.map(&:name) の形式で呼び出せるのはpublicメソッドだけ

Rubyの仕様として、users.map(&:name)&:nameの部分にあたるメソッドはpublicメソッドしか呼び出せません。
protectedメソッドは呼び出せないので、最初に示したコードのように、

users.map { |u| u.name }

通常のブロックで呼び出す必要があります。

おまけ:Ruby 3.2以降で微妙に変わった言語仕様

ところで、users.map(&:name)のような呼び出し方は、以下のようなコードを書いているのとほぼ同じです。

# レシーバ u に対してメソッド :name を呼び出す
users.map { |u| :name.to_proc.call(u) }

実は :name.to_proc.call(u) のような呼び出し方は、Ruby 3.1までエラーなく呼び出せていました。

RUBY_VERSION
#=> "3.1.2"

class User
  def fetch_names(users)
    # Ruby 3.1 まではエラーなく呼び出せていた
    users.map { |u| :name.to_proc.call(u) }
    
    # ただし、これは3.1以前でもNG
    # users.map(&:name)
  end
  
  protected
  
  def name
    'Alice'
  end
end

users = [User.new, User.new, User.new]
users[0].fetch_names(users)
#=> ["Alice", "Alice", "Alice"]

しかし、Ruby 3.2以降ではエラーが起きます。

RUBY_VERSION
#=> "3.2.2"

# (略)

users = [User.new, User.new, User.new]
users[0].fetch_names(users)
#=> protected method `name' called for an instance of User (NoMethodError)

これは下記issueでの議論の結果、シンボルをproc化したときの可視性チェックが非常に難しいため、一律で「publicメソッド以外は呼び出せないようにする」という仕様変更が入ったためです。

In general, a.b(&:c) should behave exactly the same as a.b{_1.c}. But visibility check for protected methods may be too difficult for to_proc. So rejecting protected methods altogether like private methods is acceptable.

Matz.

この仕様変更、僕は把握してなかったんですが、Ruby 3.2のNEWSには書いてなかったみたいです。(バグ修正扱いだったせいでしょうか? :thinking:

まとめ

というわけで、この記事では users.map(&:name) の形式で呼び出せるのはpublicメソッドだけ、という話を書いてみました。

protectedメソッドを取り扱う機会はあまりないと思いますが、protectedメソッドは users.map(&:name) では呼び出せないということを頭の片隅に置いておくと、いつか役に立つときが来るかもしれません(いや、来ないかな……😅)。

Special thanks

ruby-jpで上記issueの存在を教えてくださった @tompng さん、どうもありがとうございました!

16
5
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
16
5