読書

「プロを目指す人のためのRuby入門」を再読した

Railsを使っているとしばしばRuby自体を学びなおした方が良いのではと思うことがあり、Ruby初心者の頃に読んだチェリー本を読み返してみた。

Rubyに初めて触れてから3年くらいが経つものの、業務で使わない構文や機能に関しては頭から飛んでいたことがよくわかった。長期記憶に焼き付けておく目的で、重要度が高く記憶の定着度が低いものを書き残しておく。

どんな本か

プログラミング言語『Ruby』の入門書。プログラミングそのものの入門者は対象としておらず、ある程度プログラミング経験がある人を対象読者としている。注意点として、RubyのWebアプリケーションフレームワーク『Ruby on Rails』は本書の範囲外。

読者の疑問を先読みした非常にわかりやすい説明がなされる一方で、入門書としては比較的深めの領域にも触れている。全体を通してサンプルコードがテスト駆動開発を模していたり、デバッグ技法について説明する章が設けられているなど、実務に有益な情報が多く盛り込まれているのも特徴的。

2017年12月8日発行。技術評論社。著者は株式会社ソニックガーデンのプログラマ 伊藤淳一氏。

https://gihyo.jp/book/2017/978-4-7741-9397-7

覚えてなかったところ

splat展開

こういうやつ

(1..5).to_a #=> [1, 2, 3, 4, 5]

[*1..5] #=> [1, 2, 3, 4, 5] 

Rangeオブジェクトの前に*をつけることでRange範囲内の値を展開してくれる。

他にも配列展開して複数の引数として渡すときにも使える。

a = [] #=> []
a.push(1,2) #=> [1, 2]
a.push([3,4]) #=> [1, 2, [3, 4]]
a.push(*[5,6]) #=> [1, 2, [3, 4], 5, 6]

配列を入れ子にしたくない時はflattenを使っていたけど、splat展開で済む場合も多そう。

でもこれ、前者はRangeに対してで、後者は配列の引数渡しで使っているので実際別の動きをしてそうだな、と思って検索したら同じことを考えていた方がまとめてくださっていた。

https://ryotatake.hatenablog.com/entry/2018/11/20/splat_operator

変数代入するときに、右辺に*を付けると配列変換として機能するということらしい。Integerのリテラルにも適用されるということは、初心者が自己代入構文の順番間違えて誤爆するなんてこともありそうだなと思った。

a = 100 #=> 100
a *= 2 #=> 200
a =* 3 #=> [3]

可変長引数を受けるメソッドを定義する時にも*を使うそう。たぶん上で使ったpushメソッドの定義でも使われてるのだろう。Javaだと(String... args)で書いてたのを思い出した。

throw, catchでの大域脱出

ネストされたループを一気に抜ける構文。breakはひとつ外にしか出られないので、多重ループでたくさんbreakを書いて可読性が下がりそうな場合に使うと良さそう。catchの分だけネストが1増えるので2重ループ程度で使うべきかは考えものかも。

count = 0
catch :done do
  (1..10).each do |i|
    ('a'..'z').each do |j|
      ('A'..'Z').each do |k|
        count += 1
        throw :done if i == 5 && j == 'g' && k == 'G'
      end
    end
  end
end

p count #=> 2867

ぼっち演算子、nilガード

Ruby2.3から導入のぼっち演算子。レシーバがnilの場合にメソッドがnilを返すようになり、NoMethodErrorが起きない。知らなかったわけではないけど、仕事で古めのRubyを扱うことが多くそもそも使えてないので備忘録として。

a = nil
a&.upcase #=> nil

b = 'ruby'
b&.upcase #=> RUBY

変数代入の時、代入先がnilの場合に代入したい時に使うイディオム。論理和の演算子と自己代入を組み合わせたもので、左辺がfalsyの時に代入される。

a = nil
a ||= 10
a #=> 10

b = 20
b ||= 10
b #=> 20

rubyはnilfalseだけがfalsyなのでいいけど、PerlやJavaScriptで使うとバグ製造機になりそう。

正規表現のキャプチャ名付け

正規表現でキャプチャした値に名前をつけておける機能。

text = '今日は2021年12月12日です'
m = /(?<year>\d+)年(?<month>\d+)月(?<day>\d+)日/.match(text)

p m[:year] #=> "2021"
p m[:month] #=> "12"
p m[:day] #=> "12"

p m #=> #<MatchData "2021年12月12日" year:"2021" month:"12" day:"12">

左辺に正規表現リテラルを、右辺にターゲット文字列を置いて=~演算子で繋ぐとローカル変数が作られるらしい。すごい。

text = '今日は2021年12月12日です'
if /(?<year>\d+)(?<month>\d+)(?<day>\d+)/ =~ text
  puts "#{year}/#{month}/#{day}"
end
#=> 2021/12/12

正規表現のパターンが使いまわしづらくなったり、Web上のチェッカーに入れづらくなりそうだなーと思ったけど、Rubularでチェックすると変数名付けも含めてチェックしてくれるみたい。

Rubularのキャプチャ

メソッドの有無を調べる

respond_to?を使うと、レシーバに対してメソッド呼び出しが可能かが確認できる。メタプログラミングには欠かせない。ただWebアプリケーションの開発で使うかというとそんなに出番は多くないのではと思う。

ちなみにRailsのActionControllerによく似たメソッドがあるけど別人。こっちはObjectクラス(Kernelモジュール)が持っているメソッド。

num = 100
num.respond_to?(:to_s) #=> true
num.respond_to?(:push) #=> false

num.method(:respond_to?).owner #=> Kernel

モジュール全般

モジュールについてはこちらの記事で振り返った。

例外の継承関係

これが頭に入ってなかったのは結構まずいのでは、と自分で思ったやつ。

すべての例外系クラスの継承元がExceptionクラス。そのExceptionを継承したStandardErrorが、通常のプログラムで発生する可能性の高い例外を表すスーパークラス。例えばNameErrorArgumentErrorなどが該当する。

StandardErrorは、rescue節に例外クラスを指定しなかった場合のデフォルトの捕捉対象。つまりは開発者が捕捉してなんとかできる可能性がある例外、と言える。JavaでいうところのExceptionクラス。

一方、NoMemoryErrorSystemExitなど、StandardErrorを継承ルートに持たないクラスが存在する。これらはどうしようもない系の例外なので一般的にコード上で捕捉しない。JavaでいうところのErrorクラスに相当する。うっかりrescueExceptionを指定しないように気をつけたい。

ensureの代わりにブロック

File.openはブロックを渡して呼び出すと、ブロック内で例外が発生した際にもclose処理を実行してくれる。Javaでいうところのtry-with-resource文。

try-with-resourceの場合はjava.lang.AutoCloseableが実装されているかを確認すればよかったが、rubyの場合はメソッド仕様を知らないと自動クローズしてくれることに気付かないかも。

map(&:upcase)の意味

mapとかinjectでよく使うこれ。なぜこれでブロック渡したときと同じ結果になるのか、「そういうもの」的な理解しかしていなかった。

['ruby', 'java', 'perl'].map { |s| s.upcase } #=> ["RUBY", "JAVA", "PERL"]
['ruby', 'java', 'perl'].map(&:upcase) #=> ["RUBY", "JAVA", "PERL"]

&は右辺のオブジェクト(この場合は:upcaseのシンボル)に対してto_procメソッドを呼び出し、戻り値のProcオブジェクトをmapメソッドに引数として渡している。シンボルから作ったProcは、第一引数をレシーバにしてシンボルで指定したメソッドを呼び出す。らしい。

結局「そういうもの」の理解からはあまり脱しない気もするけど、単なるおまじないではなく裏に理由があるのだということを認識しているのが大事だと思う。

tap

メソッドチェーンの途中の値をprintデバッグしたい時に便利なメソッド。tapはレシーバをそのままブロックに渡し、ブロックの戻り値はレシーバになるので、Proxy的な動きをしてくれる。

'ruby'.upcase.delete('Y').concat('O') #=> "RUBO"

#delete時点の値を見たい
'ruby'.upcase.delete('Y').tap { |s| puts s }.concat('O') #=> RUB

source_location

そのメソッドが定義されているライブラリのコードがどこにあるかを確認するメソッド。便利だけど多分GitHub見に行ってしまう自分がいる。

標準ライブラリなど、ソースが手元に存在しないものはnilを返す。

'string'.method(:underscore).source_location
#=> ["/usr/local/bundle/gems/activesupport-6.1.4.1/lib/active_support/core_ext/string/inflections.rb", 143]

感想

改めて入門書を読んでみると、実務で使う範囲の構文しか身に付いていないことや、「なんとなくそういうもの」の理解で済ませている要素が色々あることに気付かされた。

言語仕様を深く理解しないままでもフレームワークを使えてしまうけど、フレームワーク自体のコードを読もうと思ったら言語仕様の理解は不可欠で、中級者以上は避けて通れない道なんだろう。

そのうち「メタプログラミングRuby」を読み直そうと思う。


<< 記事一覧へ