Rubyコミッターの須藤です。
2023年4月に「すでにRubyをよく知っている」人向けの書籍研鑽Rubyプログラミングが出版されました。私はRubyをよく知っているので読む資格があるはず!
内容
Jeremy Evansさんはもりもりコードを書いているRubyコミッターです。そんな人が書いた内容なので、上級者なら知っていそうだけど中級者は知らないかも?というレベルの情報がバンバン出てきます。初心者はついていけないでしょう。あるいは書いている内容を鵜呑みにしてしまうかもしれません。
しかし、ここに書いている内容を鵜呑みにしてはいけません。サブタイトルが「実践的なコードのための原則とトレードオフ」とある通り、一部は「原則」としてベースの考えとしてもいいものですが、そうでないものは「トレードオフ」として考えなければいけません。「トレードオフ」ということはあちらを優先するとこちらがおろそかになるということです。ケースバイケースで適切なバランスを見つける必要があるということです。たとえ中級者でも本に書いていることを鵜呑みにする傾向があるRubyistはやめた方がいいかもしれません。
全体的に「知っておくと適切なトレードオフを考える材料が増えてよさそう」という情報が詰まっているので自分で判断できる中級者以上のRubyistは読むとよいでしょう。
なお、この本がおもしろかった人は次にAPIデザインケーススタディを読んで欲しいです。また違った視点の判断材料が増えるはずです。
私が思ったこと
せっかくなので読んでいて私が思ったことを紹介します。これもあなたのトレードオフを考える材料に加えてみてください。
17ページ:each_value.to_a
はムダじゃない?
17ページあたりでは曲を検索する機能を例にしていろいろ説明しています。その中に次のコードがあります。
lookup = ->(album, track=nil) do
if track
albums.dig(album, track)
else
a = albums[album].each_value.to_a
a.flatten!
a.uniq!
a
end
end
私が気になるのはeach_value.to_a
です。私ならvalues
と書きます。そっちの方が直感的でわかりやすいですし、たぶん、Enumerator
オブジェクトができないので速い気がします。本書ではちょいちょい「こっちの書き方よりあっちの書き方の方が速い」という話がでてくるので、なぜここでeach_value.to_a
しているかわかりませんでした。
32ページ:alias_method :prepend, :include
はわかりにくいよー
32ページあたりでは「オープン・クローズドの原則」をRubyで実現する方法を紹介しています。その中に次のコードがあります。
class OpenClosed
def self.meths(m)
m.instance_methods + m.private_instance_methods
end
def self.include(*mods)
mods.each do |mod|
unless (meths(mod) & meths(self)).empty?
raise "class closed for modification"
end
end
super
end
singleton_class.alias_method :prepend, :include
end
OpenClosed.include
とOpenClosed.prepend
の両方で同じチェックをしてから元のメソッドを呼び出したいというコードです。
本文ではsuper
はエイリアス後の名前が使われるのでこれで動くとあるのですが、動いていない気がします。Ruby 3.0だと動いていたのかしら。
class OpenClosedAlias
def self.meths(m)
m.instance_methods + m.private_instance_methods
end
def self.include(*mods)
mods.each do |mod|
unless (meths(mod) & meths(self)).empty?
raise "class closed for modification"
end
end
super
end
singleton_class.alias_method :prepend, :include
end
class OpenClosedDefine
def self.meths(m)
m.instance_methods + m.private_instance_methods
end
def self.include(*mods)
mods.each do |mod|
unless (meths(mod) & meths(self)).empty?
raise "class closed for modification"
end
end
super
end
def self.prepend(*mods)
mods.each do |mod|
unless (meths(mod) & meths(self)).empty?
raise "class closed for modification"
end
end
super
end
end
module A
end
module B
end
OpenClosedAlias.prepend(A)
OpenClosedAlias.prepend(B)
pp OpenClosedAlias.ancestors
OpenClosedDefine.prepend(A)
OpenClosedDefine.prepend(B)
pp OpenClosedDefine.ancestors
$ ruby -v /tmp/a.rb
ruby 3.3.0dev (2023-04-23T03:01:13Z master dafbaabc04) [x86_64-linux]
[OpenClosedAlias, B, A, Object, PP::ObjectMixin, Kernel, BasicObject]
[B, A, OpenClosedDefine, Object, PP::ObjectMixin, Kernel, BasicObject]
仮に動いていたとしてもこのコードは私にはわかりにくいので私はこういうコードは書きません。私なら共通部分をメソッドに切り出してそれぞれのメソッドから使うようにします。
class OpenClosed
def self.meths(m)
m.instance_methods + m.private_instance_methods
end
def self.ensure_closed(*mods)
mods.each do |mod|
unless (meths(mod) & meths(self)).empty?
raise "class closed for modification"
end
end
end
def self.include(*mods)
ensure_closed(*mods)
super
end
def self.prepend(*mods)
ensure_closed(*mods)
super
end
end
60ページ:*_list
がイヤ
60ページあたりはローカル変数の命名について説明しています。その中に次のコードがあります。
options.each do |key_list, value_list|
key_list.each do |key|
value_list.each do |value|
p [key, value]
end
end
end
私はここのkey_list
とvalue_list
がイヤです。RubyにList
クラスはないからです。リストじゃないものに_list
とつけるのがイヤです。
だったらkey_array
とvalue_array
ならいいのかというとそれもイヤです。このkey
/value
のコンテナーがArray
でなければいけない!!!というのなら、まぁ、key_array
/value_array
でもいいですけど、「複数のキーである」・「複数の値である」という情報で十分であることが多いので私はkeys
/values
がいいです。
options.each do |keys, values|
keys.each do |key|
values.each do |value|
p [key, value]
end
end
end
でもねぇ、_list
を使いたくなるときもあるんですよねぇ。どういうときかというと複数のkeys
があるときです。keys
をさらに複数形にできないんですよねぇ。なので、 https://github.com/ruby/rss ではイヤだなぁと思いながらも_list
を使っちゃっています。どういう名前付けがいいんだろうなぁ。
追記:_list
じゃなくて_items
は?案が出ました。よさそう。
66ページ:インスタンス変数のスコープはself
と一緒って説明すればよかったのか!
ここまでは私の考えと違うということを紹介しました。今度は「なるほどー」と思ったところを紹介します。
66ページあたりではインスタンス変数のスコープについて説明しています。そこで「インスタンス変数のスコープは、メソッドの暗黙的なレシーバー、つまりself
と必ず一致します」と説明していました。
別に私がインスタンス変数のスコープを説明する機会が多くてそのたびになんて説明すればいいか悩んでいたというわけではないのですが、こう説明すればいいということを知らなかったので「なるほどー」と思ったのでした。
137ページ:例外のバックトレースのコスト
次はまた私の考えと違うところになります。
137ページあたりでは例外の実行性能について説明しています。その中でバックトレースのコストが大きいのでバックトレースを無効にすることで高速化できるという説明があります。
この場合は私ならcatch
/throw
を使います。本文中でも言及されていますがバックトレースがないとデバッグがすごく難しくなります。それを防ぐために必ずその例外を処理しろと書いてあります。そのため、バックトレースをなくしてでも高速化したいときは範囲がわかった上で大域脱出をしたいときなはずです。
Rubyでcatch
/throw
を使うことは滅多にないのでそもそもこの機能があることを知らない人の方が多そうな気はします。私は大域脱出のためにたまに使います。
たとえば、grntestという全文検索エンジンGroonga用のテスティングフレームワークで使っています。
テスト開始前にcatch
してテスト中に中断したいときにthrow
しています。たとえば、テスト中にテスト環境を確認して前提条件を満たしていないときにthrow
して中断します。
他にもコマンドラインオプションの解析を途中でやめるときに使っています。具体的には--help
を見つけたらその時点でバージョンを出力して解析をやめます。そのときは次のようにthrow(tag, true)
としてtrue
を返すようにしています。このメソッドはテストが成功したらtrue
、失敗したらfalse
を返したいです。--version
は未知のコマンドラインオプションではないのでtrue
を返したいケースです。OptionParser
はデフォルトで--version
が動くようになっているのですが、そこではexit
するようになっていてテストしにくいのでrun
を終了するだけにしています。例外でも実現できますが、大域脱出のスコープが明確になるので私はこういうときはcatch
/throw
を使っています。(例外のバックトレースコストを気にしているわけではないです。)
def run(argv=ARGV.dup)
catch do |tag|
parser = create_option_parser(tag)
parser.parse(argv)
run_test
end
end
def create_option_parser(tag)
parser = OptionParser.new
parser.on("--version", "Show version and exit") do
puts(VERSION)
throw(tag, true)
end
parser
end
189ページ:プラグインシステムのデメリット
189ページあたりはプラグインシステムのメリットを説明しています。しかし、ここでデメリットは説明していません。トレードオフを提供するという本書の方針に合わせてここでデメリットも説明して欲しかったです。ちなみに、そのまま読み進めたらデメリットも説明してありました。
209ページ:メタプログラミング
209ページからはメタプログラミングの適切な使いどころを説明しています。私は、上級者なら節度を守ってメタプログラミングを使って欲しいので、メタプログラミングの使いどころも説明するのは「わかっているな!」と思いました。
参考情報として私の判断基準も共有しておきます。それはメタプログラミングをして割に合うかの判断基準:処理を1箇所に局所化できるかです。
225ページ:ドメイン特化言語
225ページからはドメイン特化言語(DSL)について説明しています。RubyはDSLを作りやすい言語なので中級者以上ならたしかに知りたい話題だろうなぁと思います。
参考情報として私のDSL設計の知見を共有します。それはRubyで自然なDSLを作るコツ:値を設定するときはグループ化して代入です。
230ページ:SequelのAPI
230ページあたりではSequelを例にしてDSLの設計を説明しています。たしかにSequelのAPIはよくできているのでそれを汎用的にしたテーブル操作ライブラリーAPIをRed Data Toolsで提供しようと思っています。red-table-queryがそのライブラリーのリポジトリーなのですが、構想だけでまだ手を動かせていません。今年こそは実装したいな。。。
241ページ:パイプをクローズしていない
241ページあたりではRubyスクリプトの構文エラーをチェックする話をしています。その中に次のコードがあります。
Dir['/path/to/dir/**/*.rb'].each do |file|
read, write = IO.pipe
print '.'
system('ruby', '-c', '--disable-gems', file,
out: write, err: write)
write.close
output = read.read
unless output.chomp == "Syntax OK"
puts
puts output
end
end
read
のパイプをクローズしていないのが気になります。私は、ファイルを開くときにFile.open {...}
を使うように、パイプを開くときはIO.pipe {...}
を使うべき派です。
あと、私は、system
の出力をパイプで受けるのは避けたい派です。出力が多すぎるとパイプのバッファーが埋まってしまって刺さってしまうからです。それを避けるには別スレッドでパイプから読み込み続けないといけないのですが、そうするとsystem
が同期APIで(非同期APIより)読み書きしやすいという利点が失われてしまいます。なので、出力サイズが予測できないとき・大きくなるかもしれないとき(たとえばzstdcat
の結果を受け取るとか)は、私はTempfile
でファイルに出力しています。ファイルIOが発生するので遅くなるのですが、速度やストレージ容量を気にしなくてよいときはTempfile
にします。
242ページ:テスト手法
242ページからはテスト手法のことを説明しています。その中で「開発後テスト」・「テスト駆動開発」・「ビヘイビア駆動開発」を説明しています。説明を読む感じではテスト駆動開発とビヘイビア駆動開発がキライなのかな?という印象を持ちました。
269ページ:経験豊富なプログラマーは機能削除がうれしい
269ページあたりは機能の削除について説明しています。その中で次のように書いてあります。
新米プログラマーは「機能を削除する」と聞くと、何か良くないことのように思うかもしれません。しかし経験豊富なプログラマーにとっては最も喜ばしい瞬間のひとつです。
そうなんですよ!
270ページ:__callee__
を知らなかった
270ページあたりはメソッドの削除方法を説明しています。その中で次のコードがありました。
def method_to_be_removed
warn("#{__callee__} is deprecated",
uplevel: 1, category: :deprecated)
# ...
end
私は__callee__
があることを知りませんでした。__method__
はたまに使うのでそれと同じかと思ったのですが、エイリアスされているときの挙動が違いました。__callee__
はエイリアス先の名前になって__method__
はエイリアスされても元の名前が返ってきます。
to_enum(__method__)
で__method__
を使っていたんですが、これは__callee__
を使うほうがいいのかしら。。。
313ページ:ウェブプログラミングの話いらなくない?
313ページからは第III部としてウェブプログラミングの話になります。著者が開発しているSequelとRodaを紹介したかったんだろうなぁとは思うのですが、この部はまるごと無い方がいいんじゃないかと思いました。
書くなら、この部を切り出してこの話題の専門書として分けてより周辺の情報も足してまとめた方がよい気がします。「Rubyプログラミング」という本書のスコープの範囲で書くとこのくらいの内容になってしまうのはしょうがないと思います。私の感覚ではこの話題は「Rubyプログラミング」というスコープから外れていると思うので抜いたほうがよかったと思いました。(経験豊富なプログラマーは機能削除がうれしい!)
363ページ:ブログを書く時間でバグを修正したほうがよい
363ページからは訳者あとがきになっています。その中で著者は「ブログを書く時間でバグを修正したほうがよい」というスタンスであるというエピソードが紹介されています。すごくその気持ちがわかります!今、私はブログを書いているけど!
私は「ドキュメントを書く時間でバグを修正したほうがよい」とも思ってしまっているのですが、著者はドキュメントをしっかり書いているので偉いなぁと思っています。
まとめ
2023年4月に研鑽Rubyプログラミングが出版されたので、その内容と私の感想を紹介しました。たしかに、(自分で判断できるはずの)中級者以上のRubyistに読んで欲しいです。この本がおもしろかったRubyistにはAPIデザインケーススタディも読んで欲しいです。
来月開催のRubyKaigi 2023には著者のJeremy Evansさんも訳者の角谷さんも参加するので読んだ人は感想を伝えましょう!ついでにサインももらうといいでしょう!