Effective Ruby 第五章 メタプログラミング まとめ その1
Effective Ruby 第四章 例外 まとめ - Memento memo. の続きです。
第五章 メタプログラミングについての前半部です。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (4件) を見る
モジュール、クラスフックを使いこなす
- Rubyのイベント通知 -> フック関数の実行は、適切な名前のメソッドを書くだけで実行可能
- 定義できるフックは10種類
hookメソッド | タイミング |
---|---|
included | moduleがincludeされる |
extended | moduleがextendされる |
prepended | moduleがprependされる |
inherited | classが継承される |
method_added | method追加 |
method_removed | method削除 |
method_undefined | method定義解除 |
singleton_method_added | 特異method追加 |
singleton_method_removed | 特異method削除 |
singleton_method_undefined | 特異method定義解除 |
- 全てのフックメソッドは特異メソッドとして定義する
- method追加、削除、定義解除系のフックは引数としてメソッド名を受け取る。クラス名を知りたい場合はselfを使う。
- 全てのフックメソッドは自動的にprivateになる
- フックメソッドに関連する以下のメソッドはオーバーライドしてはならない。フックを使うこと。
- extend_object
- append_features
- prepend_features
module PreventInheritance class InheritanceError < StandardError; end def inherited(child_klass) raise(InheritanceError, "#{child_klass} cannot inherit from #{self}") end end ::Array.extend(PreventInheritance) class ChildArray < ::Array; end #=> ChildArray cannot inherit from Array (PreventInheritance::InheritanceError)
inherited
フックを使うとこんな感じの使い方になります。
クラスフックからはsuperを呼び出す
- hookは他のモジュールで定義されたhookの制御を完全に奪ってしまう場合がある。
- クラスフックメソッド内では必ず
super
を呼ぶのが行儀が良い。
method_missingではなくdefine_methodを使う
- method_missingを使ってはいけない理由
- パフォーマンス面でコストがかかる
- エラーメッセージが分かりにくくなる
respond_to?
等のイントロスペクションメソッドが使えなくなる。
- method_missingで本来やりたいことの大半はproxy, decorator patternの実装だが、これらはdefine_methodで代替可能
- proxyの実装
class HashProxy Hash.public_instance_methods(false).each do |name| define_method(name) do |*args, &block| @hash.send(name, *args, &block) end end def initialize @hash = {} end end
- decoratorの実装
require('Logger') class AuditDecorator def initialize(object) @object = object @logger = Logger.new($stdout) @object.public_methods.each do |name| define_singleton_method(name) do |*args, &block| @logger.info("calling '#{name}' on #{@object.inspect}") @object.send(name, *args, &block) end end end end
上記例の用に、オブジェクトの場合は define_singleton_method
で特異メソッドを定義できます。
- どうしても
method_missing
を避けられない場合はrespond_to_missing?
を併用する。 respond_to_missing?
はrepond_to?
がメソッドを見つけられなかった場合の最後のチャンスをプログラマに委ねる。
evalの多様な変種間の違いを把握する
eval
,instance_eval
,class_eval
で直接文字列を評価するのは避ける。- 代わりに
class_exec
,instance_exec
とブロックを使うと変数のvalidation等が行えるようになる。
参考
細かいところはメタプログラミングRubyを読む必要がありそうですね。
- 作者: Paolo Perrotta,角征典
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/10/10
- メディア: 大型本
- この商品を含むブログ (2件) を見る
続きます。
VegetaでPhoenixとRailsの負荷テスト
以前会社の同期とご飯を食べていた時、 HTTP負荷テストツールのVegetaというものを紹介してもらったので試してみました。
GitHubのプロジェクトページを開くとイカしたサイヤ人の王子が目に飛び込んできます。
Golang製ツールで、CLIで比較的簡単に扱えるのが特徴のようです。
大量のリクエストを投げるコマンドが vegeta attack
です。
大量にエネルギー弾を撃ちまくるイメージですね。
ドラゴンボールの負け確フラグです。
"王子戦法"、またの名を"グミ撃ち"というらしいです。
Vegeta で負荷をかける
とりあえず使ってみます。他所のサーバに負荷をかけると本当に怒られるのでやめましょう。自前で用意したサーバかlocalhostに向かって実行しましょう。
負荷テストの対象は最近ハマってるPhoenix (v1.2.1, Elixir 1.2.6)と、比較用のRails (v5.0.0.1, Ruby 2.2.3)です。
手持ちのMBPのローカルでPhoenixとRailsをnewした状態でサーバを立てます。 そしてlocalhostのルートURLに向かってリクエストを飛ばします。
※ 条件は超適当です。
VegetaのREADMEを読みつつ、以下のようなコマンドで rate
を変えながら結果グラフを作成してみます。
$ echo "GET http://localhost:3000" | vegeta attack -duration=10s -rate=100 | vegeta report -reporter=plot > rails-100.html
vegeta attack
のオプションで以下の内容を指定しています。
- duration=10s ... 10秒間負荷をかける(今回は全試行で固定値)
- rate=100 ... 秒間リクエスト数(変動値)
Rails
※Rails5のデフォルト設定のままPumaを使っています。
チューニングすればもう少しなんとかなるかもしれません。
30req/s
余裕ですね。20ms付近で安定しています。
50req/s
まだまだいけそうです。
80req/s
ダメになりました。 半分くらいの処理が詰まって10秒遅れて処理されてます。(リクエスト受け付けずにVegetaがRetryしてるんでしょうか???HTTPステータスコード等を全く見てないので正確な挙動は不明です。すみません。。。) 通常のレスポンスも70ms程度まで落ち込んでいます。
100req/s
すごいことになりました。
Phoenix
次いでPhoenixです。
30req/s
余裕です。
100req/s
まだまだいけます。Railsはここで死んでました。
500req/s
レイテンシが若干大きくなってますが、概ね100ms以下で捌き切ってます。
800req/s
レイテンシが100ms前後安定してます。
1000req/s
エラーがいろいろ発生し始めました。 最初の2秒くらいは頑張ってますね。
2000req/s
半分くらいエラーになってしまいました。お疲れ様でした。
まとめ
条件は超ざっくりですが、ローカル環境では
くらいいけました。Phoenixの方が10倍くらいパフォーマンス良いみたいな通説があるんですが、大体そんな感じの結果です。
分散環境でもPhoenix + Elixirは強いはずなので、高速で大量のリクエストを捌く必要のあるシステム(ソシャゲやtwitter)に向いてる気がします。通信回数控えめなエンタープライズ系だとメリットより導入コスト・リスクの方が上回るような感覚です。
キャパシティプランニング ― リソースを最大限に活かすサイト分析・予測・配置
- 作者: John Allspaw,佐藤直生,木下哲也
- 出版社/メーカー: オライリージャパン
- 発売日: 2009/03/19
- メディア: ペーパーバック
- 購入: 9人 クリック: 61回
- この商品を含むブログ (21件) を見る
あと、あまり負荷テストとかやったことがないので、 このあたりもう少し勉強していきたいです。 グラフの解釈等間違っていたらご指摘いただけると幸いです。
- 作者: Dave Thomas,笹田耕一,鳥井雪
- 出版社/メーカー: オーム社
- 発売日: 2016/08/19
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
Effective Ruby 第四章 例外 まとめ
Effective Ruby 第三章 コレクション まとめ - Memento memo. の続きです。
今回はRubyの例外についてです。
例外は、以下の2つの異なる言語機能をまとめたものだと考えられます。
- エラーの説明
- 制御フロー
raiseにはただの文字列ではなくカスタム例外を渡す
raise
に文字列だけを渡さず、例外のクラス名を指定する- または例外クラスを(パラメータと共に)インスタンス化してraiseに渡す
- カスタム例外クラスにinitializeメソッドを定義するときはsuperを呼び出す
raise("something wrong") # NG raise(CustomError, "something wrong") # OK raise(CustomError.new(param)) # OK raise(CustomError.new(param), "something wrong") # OKだが、第一引数のオブジェクトのメッセージが失われる
- 新しい例外クラスは、標準例外クラス(主にStandardError)を継承し、慣例的に名称をXxxErrorのようにする。
- 1つのプロジェクトに複数例外クラスを作る場合はStandardErrorを継承した基底クラスを作成し、個々の例外クラスはそこから継承する。
出来る限り最も対象の狭い例外を処理する
- 修復プロセスがわかっている特定の例外(DB、NW接続エラー等)だけをrescueで捕まえる
- 限定された例外(詳細度の高いもの)から順にrescueする
- 汎用例外クラスをrescueで補足してはダメ。ensure節を作るか、上流で処理すること。
- rescue節内で例外が発生すると新しい例外が現在の例外を押しのけてしまう。その場合、新しい例外より元の例外をraiseするのが良い。
リソースはブロックとensureで管理する
- メモリはGCで処理されるが、メモリ以外のリソース(オープンしたファイル等)の自動的な開放は保証されない。
- ensure節でリソースを確実に開放する
- ensure節の式はbegin本体と同じスコープだが、変数が初期化されていない可能性に注意する
# ensureによるリソース管理 begin file = File.open(file_name, 'w') ... ensure file.close if file end
- ブロックで抽象化して同等の処理が可能
File.open(file_name, 'w') do |file| ... end
- File::openと同じようなブロック&ensureパターンを独自のリソース管理クラスでも実装すると良い(以下のような実装)
ensureは最後まで実行して抜ける
- ensure節の中で制御文(return, throw, next, break)などの制御文は絶対に使ってはならない
- ダメ、絶対
retryで回数上限を設け、頻度を変化させ、オーディットトレイルを残す
- 無条件のretryは無限ループに等しい危険性がある
- retry実行の境界変数はbegin節の外のスコープに定義する
- retryを使うときはaudit trail(監査証跡)を作ること。エラーのイベント連鎖を必ずログに残す。
- retry時のディレイはrescue節の中で値を(指数関数的に)増やしていくことを検討する。
スコープから飛び出したいときはraiseではなくthrowを使う
- catchとthrowは例外とは関係ない。gotoの安全versionと考える方が近い。
- 複雑な制御フローが必要な場合はraiseよりthrowを使う。throwの場合スタック上位にオブジェクトを送ることができる。
- ただしthrow、catchを多用してはならない。出来る限り単純な制御構造(return)を使う。
ネストの深いループから一撃で脱出したい場合はthrowすると良いみたいです。ただ、ネストの深いコードは循環的複雑度が高くなっちゃうのでそもそもそういったコードは避けるべきですね。
Effective Rubyは八章構成なので、前半戦終わりです。
次はメタプログラミングです。一番重そうな章です。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (3件) を見る
Effective Ruby 第三章 コレクション まとめ
Effective Ruby 第二章まとめ その2 - Memento memo. の続きです。
第三章はコレクションです。コレクションをうまく扱えるかどうかはコードの可読性・保守性にダイレクトに効いてくる気がするので、言語ごとのクセを把握しておきたいです。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (3件) を見る
コレクションを書き換える前に引数として渡すコレクションのコピーを作っておく
- Rubyのメソッド引数は値渡しではなく参照渡し。例外あり。(Fixnumは値渡しされる)
- cloneとdupメソッドでオブジェクトのコピー作成ができる。たいていdupを使ったほうがいい。ただしshallow copyになってしまうので注意。
- dupと比較して、cloneは"レシーバのフリーズ状態"と"特異クラス"を含めてコピーする
- Marshalでシリアライズ、デシシアライズすればdeep copyになるが、メモリ効率、パフォーマンスの観点が悪いので注意。
破壊的メソッドを使って要素を直接書き換えるようなコードを避ければ大半は回避できそうな気がします。
nil、スカラオブジェクトを配列に変換するにはArrayメソッドを使う
- Array(foo)とすると、fooがnilでもスカラオブジェクトでも配列に変換できる。
- Array(foo)のfooにHashを渡してはいけない
foo = nil puts Array(foo).map(&:to_s) #=> no error puts foo.map(&:to_s) #=> error!!!
ある程度柔軟に引数渡せるようになるのがありがたいです。
要素が含まれているかどうかの処理を効率よく行うために集合を使うことを検討する
- include?メソッドで要素が含まれてどうかの判定を繰り返し行うような場合は、include?のレシーバをSetクラスのインスタンスにすると高速になる。
- SetはArrayと同じInterfaceを持つ。
require('set')
して使う。
Array, HashだけじゃなくてSet, Rangeの存在も忘れないようにしましょう。
reduceを使ってコレクション畳み込む
- select, mapメソッドよりreduceを使った方がメモリ効率が良い。
- accumulatorの初期値は必ず与えること(コレクションが空の時の挙動を制御するため)
selectやmapだと効率が悪い、みたいなことが結構書いてあったのですが、内部実装どうなってるんでしょう。確かにreduceはあまり無駄がなさそうですが、向き不向きがある気がするのでいろんなパタンでベンチマーク取ってみようかと思います。
畳み込み自体の概念や実際の使い方については、HaskellやElixirみたいな関数型言語系の解説書を読むと理解が格段に深まるのでElixir本を読みましょう。
Hashのdefault値を利用することを検討する
- Hashのデフォルト値・デフォルトブロックを使うことを検討する(以下参照)
h = {} # デフォルト値なし i = Hash.new(0) # デフォルト値あり j = Hash.new{|hash, key| hash[key] = []} # デフォルトブロック(危険な挙動があるので注意)
- Hashのキー存在チェックは
has_key?
メソッドを使う。デフォルト値がnilとは限らないため。 - デフォルト値より
Hash#fetch
の方が安全な場合がある
h = {} h.fetch(:a) #=> KeyError h.fetch(:a, "default_value") #=> "default_value"
Hash#fetch
が一番柔軟でよく使う気がします。
Collection classからの継承よりも委譲を使う
- 継承より委譲を使うこと。
- 委譲するには
require('forwardable')
してForwardable
をextendする def_delegators
クラスメソッドで委譲ターゲットと委譲メソッドを定義する。
"継承より委譲"はよく聞きますね。OOPへの深い理解がないと適切に継承を使うのは難しいです。
Effective Ruby 第二章 クラス、オブジェクト、モジュール まとめ その2
Effective Ruby 第二章まとめ その1 - Memento memo. の続きです。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (3件) を見る
構造化データにはHashではなくStructを使う
- 新しいクラスを作るほどでもない構造化データを扱う場合はStructを使う
- Struct::newを定数に代入し、定数をクラスのように扱う
- ブロックでメソッドも定義できる
Person = Struct::new(:height, :weight, :age) do def bmi weight / (height * height) end end hoge = Person.new(1.58, 45, 20) # getterでアクセスできる puts hoge.height # メソッド呼び出し puts hoge.bmi
Hashで存在しないキーを指定するとnilになってしまってデバッグが辛くなるのでError吐いてくれるStructの方が良さ気です。
モジュールにコードをネストして名前空間を作る
- 特にライブラリ等を作成する場合は、名前の衝突を避けるためにモジュール内に定義をネストして名前空間を切る。
- 名前空間の構造はディレクトリに合わせる
- トップレベル定数(
Array
とか)を使う場合に曖昧さが残る場合は"::"を使ってフル修飾する(::Array
)
ライブラリ使う際は気を付けましょう。普通に開発する場合はあまり気にならないかも?
様々な等値の違いを理解する
- "==", "===", "equal?", "eql?"の4種類の比較方法がある
- "equal?"メソッドは厳格にオブジェクト(object_id)を比較する。オーバーライド禁止。
- "eql?"メソッドはHashキーとして使われているオブジェクトの比較に使われる。たいていの場合は"=="の別名として定義した方が良い。
- "=="メソッドは2つのオブジェクトが同じ値かどうかを緩やかにテストする。
- "==="メソッドはcase式のwhen節をテストする。左被演算子がwhenに与えられる引数、右被演算子がcaseに与えられる引数であることに注意。
"<=>"とComparableモジュールで比較を定義する
サンプルはこんな感じでした。
protectedメソッドを使ってprivateな状態を共有する
- privateな状態はprotectedメソッドで共有する
- レシーバを明示してprotectedメソッドを呼び出せるのは、同じクラスのオブジェクトか共通のスーパークラスからprotectedメソッドを継承してるオブジェクトだけ
public interfaceは必要最低限にしたいので、関連クラスのオブジェクト間同士の処理はprotectedでの記述を検討しましょう。
クラス変数よりもクラスインスタンス変数を使うようにする
Effective Ruby 第二章 クラス、オブジェクト、モジュール まとめ その1
いつぞやの続きです。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (3件) を見る
一記事一章にまとめようとしたところ二章目から量が多くて詰んだので適当なところで切ることにしました。
Effective Ruby 第二章のテーマは"クラス、オブジェクト、モジュール"です。
Rubyは純粋オブジェクト指向言語とも呼ばれていて、全てがオブジェクトです。クラス、数値リテラルを含め全てオブジェクトです。クラス自体がオブジェクトなのです。
用語
- オブジェクト: 変数の入れ物(※)
- インスタンス変数: オブジェクトに状態を表現する変数
- インスタンス: 特定のクラスの実例
- クラス: メソッドと定数の入れ物
- インスタンスメソッド: クラスのインスタンス(オブジェクト)のふるまいを表現する
- クラス変数: クラスに格納された変数
- クラスオブジェクト: (クラス自体がオブジェクト)
- クラスメソッド: クラスオブジェクトのメソッド
- スーパークラス: クラス階層内の親クラスの別名
- モジュール: 制限付きクラス(newとかがない)
- 特異クラス(singleton class): クラス階層に現れる不可視クラス。全てのオブジェクトが一つ持つ。
- 特異メソッド(singleton methods: 特異クラスに格納される、クラスメソッドや特定オブジェクト専用メソッド
- レシーバ: メソッドが呼び出されるオブジェクト
(※)オブジェクトが変数の入れ物っていうのはちょっと違うような気が。 少なくとも変数と振る舞いを両方カプセル化しています。
Rubyの継承階層について
- オブジェクトは継承階層を使ってメソッドを探す
- 階層ルートまで辿ってもメソッドが見つからなければ
method_missing
メソッドを探す - includeメソッドでmoduleをmixinした場合、特異クラス(不可視)がクラス階層に挿入される
- singleton_classメソッドはレシーバのための特異クラスを返す
- singleton_methodsメソッドは特異クラスに格納された特異メソッドを返すメソッド
コード書いて実験するのがわかりやすいですね。
class Hoge def foo puts 'bar' end end hoge0 = Hoge.new hoge1 = Hoge.new hoge2 = Hoge.new def hoge2.foo puts 'piyo' end hoge1.foo #=> bar hoge2.foo #=> piyo # 特異クラス p hoge0.singleton_class #=> #<Class:#<Hoge:0x007fa93d81c480>> p hoge1.singleton_class #=> #<Class:#<Hoge:0x007fa93d81c458>> p hoge2.singleton_class #=> #<Class:#<Hoge:0x007fa93d81c430>> ### 全部違う(それはそう) # 特異メソッド p hoge1.singleton_methods #=> [] p hoge2.singleton_methods #=> [:foo]
superのふるまいが複数あることに注意する
- 継承階層の上位メソッドをオーバーライドする際はsuperキーワードで上位メソッドを呼び出せる
- 引数・括弧なしでsuperを呼ぶと呼び出し元のメソッドに渡された全ての引数を渡してオーバーライドされるメソッドの呼び出しをするのと同じ。暗黙的な挙動のため注意。
- 引数を渡したくない場合は
super()
のように括弧をつける
基本的に括弧つけた方がよさげです。
サブクラスの初期化にはsuperを呼び出す
- Rubyのサブクラスのinitializeメソッドは自動的にスーパークラスのinitializeのメソッドを呼び出さない。
- initializeメソッドも通常のメソッドルックアップ規則が当てはまる(継承階層をたどる)
- 明示的にinitializeメソッドを定義するときはinitializeメソッド内でsuperを呼ぶ。initialize_copyとかでも同様。
紛らわしい構文に注意
- Rubyはメソッド名の末尾に "?", "!", "=" 等が使えるが、 "=" は構文的に別なので注意(セッターメソッド)
- 代入と異なり、セッターメソッドは明示的なレシーバが必要
hoge = 1 # 代入 foo.bar = 2 # セッターメソッド呼び出し self.a = 3 # セッターメソッド呼び出し
- セッターメソッド以外は明示的にレシーバ指定しなくてOKなので、不要なselfを連発してはいけない。
続きます。
Effective Ruby 第一章まとめ
最近仕事でJava、趣味でElixirを書いてるのですが、来週くらいから仕事でRuby書くっぽいのでRubyの復習を始めつつブログのネタにします。というわけでEffective Rubyを読み始めました。初見です。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (3件) を見る
備忘録がてら第一章で扱われている内容について簡単に。
Rubyの真偽値
比較時にfalseを右辺に書くと左辺のObjectの挙動に引っ張られるので危険らしいです。
Rubyのnilの扱い
nil.to_i * 2 #=> 0 nil.to_s.length #=> 0 nil.length #=> error!
ただ、nilとかemptyのチェックは実際Active Supportのメソッド使うことが多そうな印象です。
Perlっぽい機能を避ける
=~
演算子とか使わずString#match
使う$:
じゃなくて$LOAD_PATH
を使う- 特殊なグローバル変数はワンライナー以外で使っちゃダメ
- 特殊なグローバル変数使いたかったら
English
モジュールで可読性を高める
"特殊なグローバル変数"、正しい日本語名が分からない。 ちょっと自信ないですが英語だとSpecial variablesでヒットします。
定数がImmutableでない
- 普通に定数書き換え可能
- Object#freezeしよう
- コレクションのfreezeは要素までfreezeする
AAA = ["foo", "bar"].freeze # NG BBB = ["foo", "bar"].map(&:freeze).freeze #OK
- 定数はモジュールにまとめてfreezeする
module Hoge CCC = 1 end Hoge.freeze
Objectの参照考えるのちょっとめんどくさいですよね。 ところでElixirのコレクションは完璧にImmutableなので分かりやすいですよ!
Warningを出す
RUBYOPT='-w' export RUBYOPT
- 実行時のWarningは
$VERBOSE
変数にtrue|false|nil
のいずれかを指定する
基本Rubocop
使いながら開発するはずなのでコンパイル時警告はあまり気にならないかも