テクノロジー部門で Ruby インタプリタの開発をしている笹田です。RubyKaigi 2024 楽しみですね。
さて、Ruby のメソッドを定義するとき、仮引数がある場合、カッコを省略することができます。
def foo(x, y) end def bar x, y end
bar
の定義の方法ですね。私は好んでこの書き方をしてたんですが、同僚の遠藤さんに「そんな書き方をしているのは今時笹田だけだ」と言われてショックを受けたので、ちょっと調べてみました。
ちなみに、カッコがないと使えないメソッド定義の方法があるので、その時には涙を呑んでカッコをつけます。
def foo(kw:) # 必須キーワード引数 end def bar(&) # 無名ブロック引数 end
補足1:Ruby では「メソッド呼び出し時にカッコをつけるかどうか」にいろいろな論争がありますが、ここでは「メソッドを定義するときのカッコ」だけ述べます。あんまり聞いたことないんだよな。
補足2:仮引数がない場合はカッコをつけないのが多いと思うので、「仮引数があるときだけ」について述べます。
慣習
Rubocop には仮引数があるときカッコをつけろ、というルールがデフォルトであるそうです。Standardrb でもデフォルト(standard/config/base.yml at 8307fa8f449f896075ccad74bf6a128ed2c26189 · standardrb/standard · GitHub)。
コーディングルールの時によく参照される気がする Cookpad でのルールでは、カッコを付けるのが must とのことです。
[MUST] メソッド定義において、引数リストの括弧は省略してはならない。ただし、引数なしメソッドを定義する場合は括弧を省略すること。
昔よく参照されていたコーディングルールでも、カッコをつけるのが must とのこと。
メソッド定義の仮引数リストには括弧を付ける。 ただし、引数がない場合は、括弧を省略する。
私は Ruby でプログラムを書く仕事に就くのは難しそうです...。
実際の調査
実際の Ruby でのメソッド定義(仮引数つき)でどれくらいカッコをつける/つけない書き方があるかを調べてみました。
調査対象は、rubygems.org に push されているすべての gem の最新版です。Ruby 開発のために遠藤さんたちが rubygems のミラーを rubygems-mirror | RubyGems.org | your community gem host を使って管理しているので、それをありがたく使いました。
だいたい 95% の定義でカッコをつけており(w/paren)、5% のコードがカッコをつけていない(wo/paren)ようです。
「古い gem はカッコをつけないことが多いんじゃないの?」(昔よりも今のほうがカッコをつけるというルールの徹底がされてきた可能性)ということで、年別に集計してみました。
意外にも「古いから多い、少ない」という傾向はありませんでした。真ん中あたりで盛り上がってますね。
ちょっと注意が必要なのは、ここでカウントしている年は最新リリース日であり、書いた日ではない、ということです(昔に最初のリリースをして、2024 年にもリリースしている場合は、2024 にカウントされています)。
実際の値はこちら。
ついでに、仮引数がないときにはどうかも一応調べてみました。
誰もつけてないかと思ったけど、3% ほどあるんですね。なお、2023年に異常な値があったので、そこは除外しています(何が異常だったかは調べていません)。
Rails のコードを見てみると、def foo() end
のように1行で書きたいときに無引数のカッコを使っている、というのを見つけました。そういうのは一行メソッド定義(def foo = nil
)で消えそうですね。いや、そうでもないのかな?(def foo() = nil
って書く?)
調査方法
調査には Prism を使ってみました。
require 'prism' class ParenFinder TYPES = { true => { true => :paren_w_params, false => :paren_wo_params, }, false => { true => :noparen_w_params, false => :noparen_wo_params, } } def self.reset $defs = TYPES.map{_2.values}.flatten.map{[_1, 0]}.to_h $defs[:files] = 0 $defs[:time] = Time.now.to_i end reset def initialize file @file = file end def defs_rec node if node.type == :def_node paren = node.lparen_loc ? true : false params = node.parameters ? true : false type = TYPES[paren][params] $defs[type] += 1 # [@file, node.slice.lines.first.chomp] else node.child_nodes.compact.each{|n| defs_rec n } end end def defs ast = Prism.parse_file(@file) defs_rec ast.value rescue Exception => e # ignore any errors end end if ARGV.empty? ParenFinder.new(__FILE__).defs name = __FILE__ else process_file = -> f do case when FileTest.symlink?(f) # ignore (depends on data) when FileTest.directory?(f) Dir.glob(File.join(f, '*')){|f| process_file.call f} when /.*\.rb$/ =~ f $defs[:files] += 1 ParenFinder.new(f).defs else # ignore other files end rescue Exception => e # ignore end name = ''.dup ARGV.each{|f| name << f process_file.call f } $defs[:time] = Time.now.to_i - $defs[:time] end pp({ name => $defs.map{|k, v| if Array === v [k, v.size] else [k, v] end } })
Prism いじってる遠藤さんに使い方を聞けたということもあるんですが、簡単に調査スクリプトが作れてよかったです。というか lparen_loc
メソッド(仮引数をくくるカッコの位置)とかすごいな。
Symlink の扱いは、調査対象ごとにいじる必要がありそうでした。
カッコなしでのメソッド定義が許されている理由
まつもとゆきひろさんに雑談がてら「なんでカッコがないメソッド定義を許してたんですか?」と聞いてみたところ、次のような理由とのことでした。
- Ruby では「コマンドっぽいメソッド」の呼び出しではカッコが省略できる。例えば
attr :ivar
など。 - そういうコマンドっぽいメソッドを定義するときは、同じくカッコがないように定義できたほうがいいんじゃないかと思って導入した。
雑談の場では、「そもそもコマンドっぽいメソッドなのか、書いてるほうは判断できない」、「コマンドっぽいメソッドを書く機会がそんなになさそう」などの意見があり、メソッド定義時にカッコを省略する機能は、無引数のときを除いて忘れられていそうだなぁ、ということがわかりました。
まとめ
「Ruby のメソッド定義時に仮引数があるとき、それをカッコでくくらないのは私だけなの?」という疑問から始めた調査ですが、「私だけではないけどやっぱり少ない」、ということがわかりました。よく知られたコーディング規約に反しているのが 5% もあるということで、それはそれで面白い結果なのかな?
カッコつけてるのを見ると、逆にちょっとカッコ悪いというか、そういう感じがするので、そういうコードを見かけたときにはどうぞ優しくしてあげてください。
ちなみに、STORES にはコーディング規約はないそうです。ですが、STOERS 社内のソースコードの .rb ファイルを1万ファイルくらい見てみると、なんと仮引数があるときカッコをつけないメソッド定義 0 でした!(ちなみに、仮引数がないときにカッコをつける定義も0) 皆さん社会規範にちゃんと則ってる。そんな行儀の良い(?)プロダクトに携わっていただける方は 採用サイト | STORES 株式会社 をご参照ください。