Memento memo.

Today I Learned.

Effective Ruby 第五章 メタプログラミング まとめ その1

Effective Ruby 第四章 例外 まとめ - Memento memo. の続きです。

第五章 メタプログラミングについての前半部です。

Effective Ruby

Effective Ruby

モジュール、クラスフックを使いこなす

  • 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等が行えるようになる。

参考

secret-garden.hatenablog.com


細かいところはメタプログラミングRubyを読む必要がありそうですね。

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

続きます。

VegetaでPhoenixとRailsの負荷テスト

以前会社の同期とご飯を食べていた時、 HTTP負荷テストツールのVegetaというものを紹介してもらったので試してみました。

github.com

GitHubのプロジェクトページを開くとイカしたサイヤ人の王子が目に飛び込んできます。

https://camo.githubusercontent.com/417a39e5a142e0877be0a7a6d7a66cb77ea21e8c/687474703a2f2f666330392e64657669616e746172742e6e65742f667334392f692f323030392f3139382f632f632f73736a325f7665676574615f62795f7472756e6b7332342e6a7067

Golang製ツールで、CLIで比較的簡単に扱えるのが特徴のようです。 大量のリクエストを投げるコマンドが vegeta attack です。 大量にエネルギー弾を撃ちまくるイメージですね。 ドラゴンボールの負け確フラグです。 "王子戦法"、またの名を"グミ撃ち"というらしいです。

dic.nicovideo.jp

Vegeta で負荷をかける

とりあえず使ってみます。他所のサーバに負荷をかけると本当に怒られるのでやめましょう。自前で用意したサーバかlocalhostに向かって実行しましょう。

負荷テストの対象は最近ハマってるPhoenix (v1.2.1, Elixir 1.2.6)と、比較用のRails (v5.0.0.1, Ruby 2.2.3)です。

手持ちのMBPのローカルでPhoenixRailsを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 => Phoenixの順で軽く検証してみます。

Rails

※Rails5のデフォルト設定のままPumaを使っています。
チューニングすればもう少しなんとかなるかもしれません。

30req/s

f:id:shotat_jp:20160925171754p:plain

余裕ですね。20ms付近で安定しています。

50req/s

f:id:shotat_jp:20160925171802p:plain

まだまだいけそうです。

80req/s

f:id:shotat_jp:20160925171810p:plain

ダメになりました。 半分くらいの処理が詰まって10秒遅れて処理されてます。(リクエスト受け付けずにVegetaがRetryしてるんでしょうか???HTTPステータスコード等を全く見てないので正確な挙動は不明です。すみません。。。) 通常のレスポンスも70ms程度まで落ち込んでいます。

100req/s

f:id:shotat_jp:20160925171816p:plain

すごいことになりました。

Phoenix

次いでPhoenixです。

30req/s

f:id:shotat_jp:20160925171412p:plain

余裕です。

100req/s

f:id:shotat_jp:20160925171516p:plain

まだまだいけます。Railsはここで死んでました。

500req/s

f:id:shotat_jp:20160925171524p:plain

レイテンシが若干大きくなってますが、概ね100ms以下で捌き切ってます。

800req/s

f:id:shotat_jp:20160925171533p:plain

レイテンシが100ms前後安定してます。

1000req/s

f:id:shotat_jp:20160925171540p:plain

エラーがいろいろ発生し始めました。 最初の2秒くらいは頑張ってますね。

2000req/s

f:id:shotat_jp:20160925171548p:plain

半分くらいエラーになってしまいました。お疲れ様でした。

まとめ

条件は超ざっくりですが、ローカル環境では

くらいいけました。Phoenixの方が10倍くらいパフォーマンス良いみたいな通説があるんですが、大体そんな感じの結果です。

分散環境でもPhoenix + Elixirは強いはずなので、高速で大量のリクエストを捌く必要のあるシステム(ソシャゲやtwitter)に向いてる気がします。通信回数控えめなエンタープライズ系だとメリットより導入コスト・リスクの方が上回るような感覚です。

キャパシティプランニング ― リソースを最大限に活かすサイト分析・予測・配置

キャパシティプランニング ― リソースを最大限に活かすサイト分析・予測・配置

あと、あまり負荷テストとかやったことがないので、 このあたりもう少し勉強していきたいです。 グラフの解釈等間違っていたらご指摘いただけると幸いです。

プログラミングElixir

プログラミングElixir

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パターンを独自のリソース管理クラスでも実装すると良い(以下のような実装)

Lock.rb

ensureは最後まで実行して抜ける

  • ensure節の中で制御文(return, throw, next, break)などの制御文は絶対に使ってはならない
  • ダメ、絶対

retryで回数上限を設け、頻度を変化させ、オーディットトレイルを残す

  • 無条件のretryは無限ループに等しい危険性がある
  • retry実行の境界変数はbegin節の外のスコープに定義する
  • retryを使うときはaudit trail(監査証跡)を作ること。エラーのイベント連鎖を必ずログに残す。
  • retry時のディレイはrescue節の中で値を(指数関数的に)増やしていくことを検討する。

retry.rb

スコープから飛び出したいときはraiseではなくthrowを使う

  • catchとthrowは例外とは関係ない。gotoの安全versionと考える方が近い。
  • 複雑な制御フローが必要な場合はraiseよりthrowを使う。throwの場合スタック上位にオブジェクトを送ることができる。
  • ただしthrow、catchを多用してはならない。出来る限り単純な制御構造(return)を使う。

ネストの深いループから一撃で脱出したい場合はthrowすると良いみたいです。ただ、ネストの深いコードは循環的複雑度が高くなっちゃうのでそもそもそういったコードは避けるべきですね。


Effective Rubyは八章構成なので、前半戦終わりです。

次はメタプログラミングです。一番重そうな章です。

Effective Ruby

Effective Ruby

Effective Ruby 第三章 コレクション まとめ

Effective Ruby 第二章まとめ その2 - Memento memo. の続きです。

第三章はコレクションです。コレクションをうまく扱えるかどうかはコードの可読性・保守性にダイレクトに効いてくる気がするので、言語ごとのクセを把握しておきたいです。

Effective Ruby

Effective Ruby

コレクションを書き換える前に引数として渡すコレクションのコピーを作っておく

  • 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. の続きです。

Effective Ruby

Effective Ruby

構造化データには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モジュールで比較を定義する

  • "<=>"演算子を実装し、Comparableモジュールをincludeする
  • "<=>"演算子は、比較できない場合はnilを返す

Version.rb

サンプルはこんな感じでした。

protectedメソッドを使ってprivateな状態を共有する

  • privateな状態はprotectedメソッドで共有する
  • レシーバを明示してprotectedメソッドを呼び出せるのは、同じクラスのオブジェクトか共通のスーパークラスからprotectedメソッドを継承してるオブジェクトだけ

public interfaceは必要最低限にしたいので、関連クラスのオブジェクト間同士の処理はprotectedでの記述を検討しましょう。

クラス変数よりもクラスインスタンス変数を使うようにする

  • クラス変数はグローバル変数と同様の問題(並行処理など)があるため、クラスインスタンス変数を使う。
  • クラスはオブジェクトのため、専用のprivate instance変数セットを持っている。
  • Singletonを作成したい場合は include(Singleton) する

Effective Ruby 第二章 クラス、オブジェクト、モジュール まとめ その1

いつぞやの続きです。

shotat.hateblo.jp

Effective Ruby

Effective Ruby

一記事一章にまとめようとしたところ二章目から量が多くて詰んだので適当なところで切ることにしました。

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を読み始めました。初見です。

Effective Ruby

Effective Ruby

備忘録がてら第一章で扱われている内容について簡単に。

Rubyの真偽値

  • false, nil以外の値は全て真(0も真)
  • falseを判定したい場合 false == 比較対象
  • nil判定は nil? メソッド

比較時にfalseを右辺に書くと左辺のObjectの挙動に引っ張られるので危険らしいです。

Rubynilの扱い

  • 基本的にnil チェック必要
  • nilはオブジェクト
  • nilは型変換すれば落ちない
nil.to_i * 2
#=> 0
nil.to_s.length
#=> 0
nil.length
#=> error!

ただ、nilとかemptyのチェックは実際Active Supportのメソッド使うことが多そうな印象です。

www.techscore.com

Perlっぽい機能を避ける

ruby-doc.org

"特殊なグローバル変数"、正しい日本語名が分からない。 ちょっと自信ないですが英語だと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使いながら開発するはずなのでコンパイル時警告はあまり気にならないかも