Effective Ruby を読んだメモをこっそり。メモなのでてきとうなことを言っている場合があります。

Effective Ruby
Effective Ruby
posted with amazlet at 15.03.29
Peter J. Jones
翔泳社
売り上げランキング: 114,152
 

感想

良い本だった。知ってることもそりゃあったけれど、それも含めて勉強になった。 ただ、MiniTest と ri, gem, bundler の章はツールの使い方って感じでちょっと読み飛ばしてしまったかな。他の章はよかった。

警告

ruby -w で警告を出せるが、rspec などのように ruby コマンドにオプションを渡せないことがある。そういう場合は RUBYOPT 環境変数を使える。

RUBYOPT=-w

グローバル変数 $VERVOSE を true に設定しても警告を有効にできる。false だと警告が減り、nil だと消える。 しかし、こちらでは $VERBOSE の設定が解釈される前の、コンパイル時の警告が出ない。

コンパイル時警告だけを出したい場合は、RUBYOPT=-w かつ $VERBOSE=nil として、実行時計画だけを出したい場合は $VERBOSE=true とし、両方出したい場合は RUBYOPT=-w とすればよい

※ しばらく、.bashrc に export RUBYOPT=-w と書いておいて警告をつぶしまくる業をしたくなったが、警告出まくって辛い

定数

Ruby の定数は変更可能

argv = ARGV

のように小文字の変数に代人してしまえば、argv[0] = 'foo' のように警告さえもなしに変更可能

clone と dup

clone は freeze 状態と特異メソッドもコピー。dup はやらない

Hash の初期値

Hash.new({}) と書いた場合、Hash の要素は全て同じ object_id を持つ {} で初期化される。これは使い物にならない。

Hash.new { {} } と書けば、要素にアクセスした時に始めてブロックが評価されるので、別の object_id を持つ {} で初期化される。

fields = {}
chunk.msgpack_each do |tag, time, record|
  channel = build_channel(record)
  fields[channel]    ||= {}
  fields[channel][tag] = build_message(record)
end

みたいに書いていたコードは

fields = Hash.new {|hash, key| hash[key] = {} }
chunk.msgpack_each do |tag, time, record|
  channel = build_channel(record)
  fields[channel][tag] = build_message(record)
end

と書けるようだ。ただし、fields[key] がデフォルトで {} になってしまうので、if fields[key] みたいなものは書けなくなる。if fields.has_key?(key) ならイケるようだ。

※ しかし、このコードは他の人に伝わるのかどうか微妙だ

rescue の中での例外

rescue の中でまた例外が起きる可能性も考えてコードを書く

その際、オリジナル例外情報が消えないように、例外インスタンスにオリジナルの例外も含めるように、と書いてあるが、Ruby 2.1 以降なら e.cause もあるのでそれを使っても良いのでは、と思った。

引数のデフォルト値

def_instance_delegator の所で変なところに食いついてしまった。シグネチャが

def def_instance_delegator(accessor, method, ali = method)

のようになっていて、第三引数のデフォルト値を、第二引数の値に設定していた。そんなことできたのか。

method_missing ではなく define_method を使おう

これは自分もやっている。method_missing でメソッドを定義したつもりになっていると、respond_to? が true を返してくれないし、そもそも全部 hook しはじめるので遅いし、メソッド typo してもわからなくなる。

respond_to? の代わりに respond_to_missing? というメソッドで調べることができるようだ。

instance_eval と class_eval

再整理

instance_eval でメソッド定義したら、特異メソッド。 Foo.instance_eval はクラスの特異メソッドなので、クラスメソッドになる。 インスタンスメソッド定義用に用意されたのがFoo.class_eval。クラスでしか使えない。

効率的なテスト

  • ファズテスト (クラッシュしないかどうかのテスト)
    • FuzzBert gem
    • 数日流す
  • プロパティテスト (仕様にそったランダムテスト)
    • MrProperty gem
    • XML Schema とか JSON Schema で入力データのスキーマを定義して、境界値テストするみたいなやつ

ri

ri Array

モンキーパッチあてているやつのドキュメントも見れるとのこと => あれ、Hash#except とかでてこないけどな

ri File::open
ri Array#pop
ri pop
ri bundler:README.md

irb

irb> irb 'foo'
irb#1(foo)> size

のようにしてそのオブジェクトのセッションを開ける (pry みたいなことが irb だけでもできたのか)

jobsfgkill でセッションを扱う

gem の上限指定

最新バージョンで動く保証がないのだから、自作 gem が依存している gem のバージョンは上限指定をしよう、と言っているが、反対したい。

gem.add_dependency 'fluentd', '>= 0.10.56', '< 0.12.0'
# gem.add_dependency 'fluentd', '~ 0.10.56'

のように書いてしまうと、v0.12 でも動くものが動かなくなってしまう。

gem.add_dependency 'fluentd'

とだけ書いてあれば、仮に v0.12 で動かないにしても、ユーザ側で Gemfile.lock をいじって回避しようがあるが、 gemspec でバージョン指定されてしまうと、ユーザの手で回避しようがなくなる。

「最新バージョンで動く保証がない」とか言ってないで「最新で動かないことがわかったら速やかに修正します」という姿勢をもったほうが良いと思ってる。

GC

近所の Ruby コミッタ氏に以前聞いたことが書いてあった。

  • Ruby はメモリプールを確保しているので、OSにメモリを返されないことがある
  • プール > ページ > スロット
  • ページが全部解放されない限り、OS にメモリを返却しない. ps や top でみたメモリ使用量が減らない
  • 2.1 では 1ページ 408 スロット

詰め詰め処理して、できるだけ空きページ作るような処理はやらないんだっけ?Java とかはやってた気がするが。

GC.stat の heap_length がページ数. その他 => https://gist.github.com/sonots/71277ef3f9b53fa87862

GC のタイミング

malloc_increase が malloc_limit を超えるとマイナーGC、olmalloc_increase が oldmalloc_limit を超えるとメジャーGC が走る。

  • malloc_limit: これを越えて malloc すると GC が発生する閾値
  • malloc_increase: GC 発生までに malloc/realloc したバイト総数 (GC 起動でリセット)
(1..1000).each do |i|
  a = [i] * 10000
  s = GC.stat
  puts "#{s[:malloc_limit]} #{s[:malloc_increase]}"
end

超えると Minor GC が走ってその度に malloc_limit が増えていることが確認できた。

16777216 16765560
23584579 672
...
23584579 23562384
33100043 672
...
33100043 33067888
33554432 672
...
33554432 672

malloc_limit が増え続けて、ヒープサイズも大きくなるかと思っていたが、33554432 よりは増えなくなった。はて。=> RUBY_GC_MALLOC_LIMIT_MAX (32MB) にあたったからだ。けっこう小さいような気もするし、1回のGCと考えれば大きいような気もするし、チューニング難しいな。

ObjectSpace.define_finalizer

GC発動時にリソース解放処理をさせる。そんなことができるのか。ただし、けっこう嵌りそう

freeze

"foo".freeze は定数と同じ扱いになる

"foo" でオブジェクトを生成してから freeze しそうに見えるが、そうはならないようだ。以前 f リテラルを導入するかどうか議論していたのはこの辺の挙動が不思議に見えるからかな、と邪推

Q. いつ、GC されるんだ?されなくなる? Symbol と同じ扱い?であれば、Symbol GC と同様のタイミングで解放される?


ループの中でオブジェクトリテラルを避けよう

(1..100).each do |i|
  %w[a b c].include?('a')
end

とかやると %w[a b c] の object が作られては GC されるので無駄処理になる。あらかじめローカル変数に保存しておくべき。これは意識してるな。

メモ化

複数行にわたるときは

@a ||= begin
end

と書ける。今まで

return @a if @a
...
...
@a = ...

と書いてたな。 ナルホド