シンボルでも文字列でもアクセス可能なHashを使おう!ActiveSupport {Hash編}

皆さんこんにちは!遅れてきた最年長ルーキー MUGENUP の osada です。

さて皆さんは初めてオープンクラスという概念を知った時、驚かれませんでしたか?私はとても驚きました!だって、基本クラスさえもオーバライド可能なんですよ?

[コラム]オープンクラスとは?

同名でクラス定義を行うと、クラスの再定義(上書き)ではなく、クラスへの追加拡張になる仕組み。

class A
  def a
    "a"
  end
end

a =  A.new
raise unless a.a == "a"

raise if a.respond_to?(:b)
# b というメソッドはない

# 同名のクラス定義は拡張になる
class A
  def b
    "b"
  end
end

raise unless a.b == "b"
raise unless a.respond_to?(:b)

# b というメソッドがある

# a というメソッドも維持される
raise unless a.respond_to?(:a)
raise unless a.a == "a"

このオープンクラス、Stringなど基本クラスさえも拡張可能なのが凄いです。Javascript の prototype みたいですよね。

ActiveSupport コア拡張

オープンクラスという特徴的な仕組みのおかげで、Rails では基本クラスなのに便利なメソッドが沢山ある、という状態になっています。それがActiveSupportのコア拡張です。

ということで、今日のお題はActiveSupportHashです。ターゲットはRails初心者の方。一緒に勉強できれば嬉しいです。

Hash 編

hash拡張のソースは、ここにありました。9ファイル、19メソッドあります。 ruby/2.0.0/gems/activesupport-3.2.16/lib/active_support/core_ext/hash

conversion.rb

まずは Hash 変換するconversion.rbです。といっても対象はxml。そういえばそんなのもありましたね!

{"foo" => 1, "bar" => 2}.to_xml

できそうな気がしてましたよね?

Hash.from_xml(<<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<hash>
  <foo type="integer">1</foo>
  <bar type="integer">2</bar>
</hash>
EOS)

できそうな気がしてましたよね?

deep_dup.rb

rubydup, clone はシャローコピーだというのはよく知られています。

シャローコピー?という方はRubyist Magazine - 値渡しと参照渡しの違いを理解するがオススメ

deep_dup は深いコピーをしてくれます。当然再帰ですよね〜。

  def deep_dup
    duplicate = self.dup
    duplicate.each_pair do |k,v|
      tv = duplicate[k]
      duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_dup : v
    end
    duplicate
  end

ところで、duplicate[k]v の値が異なることがあるのでしょうか?

deep_merge.rb

深いコピーがあるなら、深いマージもあるでしょう。そうでしょう。

こちらで興味深いのは、破壊メソッドを作っておいて、それを dup と組み合わせれば通常のメソッドになるということです。センスありますね。

  def deep_merge(other_hash)
    dup.deep_merge!(other_hash)
  end

  # Same as +deep_merge+, but modifies +self+.
  def deep_merge!(other_hash)
    other_hash.each_pair do |k,v|
      tv = self[k]
      self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v
    end
    self
  end

diff.rb

Hash の差分って、どうされてますか? Array なら マイナス[1,2] - [2,3] # => [1]が使えますが、Hash にはありません。ここでdiffの出番です

しかしこのdiff、 差分ではなく、差異なんです。

[6] pry(main)> { 1 => 2 }.diff({1 => 2, 2 => 2, 3 => 2})
=> {2=>2, 3=>2}

2つのハッシュの相違点を取り出すメソッドなんですね。実装はとてもシンプルで、ハッシュ1とハッシュ2に同じkey を見つけてvalueも同じときは消す。ハッシュ2からハッシュ1にあるkeyを消す。そしてマージです。

  def diff(h2)
    dup.delete_if { |k, v| h2[k] == v }.merge!(h2.dup.delete_if { |k, v| has_key?(k) })
  end

なんと悲しいことに、Rails4 では Deprecated になっていました!

  def diff(other)
    ActiveSupport::Deprecation.warn "Hash#diff is no longer used inside of Rails, and is being deprecated with no replacement. If you're using it to compare hashes for the purpose of testing, please use MiniTest's assert_equal instead."
    dup.
      delete_if { |k, v| other[k] == v }.
      merge!(other.dup.delete_if { |k, v| has_key?(k) })
  end

Hash#deep_diff, the recursive difference between two hashes by nikitug · Pull Request #8142 · rails/rails deep_diffを使えばいいじゃん、ということですが、まだ時期ではない、ということで、deprecatedを付けるだけになっているようです。しばらくは、diffということでしょうか。

with_indifferent_access.rb

params がシンボルでも文字列でもアクセスできて、不思議だなぁ?と思われたことありませんか? 普通は nil が返ってきます。

[8] pry(main)> h = {a: 1, "b" => 2}
=> {:a=>1, "b"=>2}
[9] pry(main)> h[:a]
=> 1
[10] pry(main)> h["a"]
=> nil
[11] pry(main)> h[:b]
=> nil
[12] pry(main)> h["b"]
=> 2

そんな時に使うのか、with_indifferent_accessです!

[13] pry(main)> h = {a: 1, "b" => 2}.with_indifferent_access
=> {"a"=>1, "b"=>2}
[14] pry(main)> h[:a]
=> 1
[15] pry(main)> h["a"]
=> 1
[16] pry(main)> h[:b]
=> 2
[17] pry(main)> h["b"]
=> 2

このメソッドは、HashWithIndifferentAccessへのプロキシメソッドです。

  def with_indifferent_access
    ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(self)
  end

それが何かと言いますと、

module ActiveSupport
  class HashWithIndifferentAccess < Hash

Hash のサブクラスでした。そして、new_from_hash_copying_default(self)

    def self.new_from_hash_copying_default(hash)
      new(hash).tap do |new_hash|
        new_hash.default = hash.default
      end
    end

となっていますので、やっていることは、元 hash の default を自身に適用しているだけです。まさに継承している感じですね。

ではコンストラクタに秘密があるのでしょうか?

    def initialize(constructor = {})
      if constructor.is_a?(Hash)
        super()
        update(constructor)
      else
        super(constructor)
      end
    end

この場合、superHashクラスの呼び出しですから、ミソはupdateにあるようです。あれ?updateってマージするメソッドだったような?

    def update(other_hash)
      if other_hash.is_a? HashWithIndifferentAccess
        super(other_hash)
      else
        other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
        self
      end
    end

ハッシュが既にHashWithIndifferentAccessだった場合は、そのままインスタンス化するだけです。するとelseが心臓部のようです。

        other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }

文字列でもシンボルでも

key 側の処理はこんな感じ。

      def convert_key(key)
        key.kind_of?(Symbol) ? key.to_s : key
      end

単に Symbol だったら文字列に変換しているんですね。ずるい!なんてシンプルな実装なんでしょうか。 このconvert_key を各所で使うため、key は全てシンボルではなく文字列になります。 でしたよね?

[13] pry(main)> h = {a: 1, "b" => 2}.with_indifferent_access
=> {"a"=>1, "b"=>2}

convert_value

value側の処理です。

      def convert_value(value)
        if value.is_a? Hash
          value.nested_under_indifferent_access
        elsif value.is_a?(Array)
          value.dup.replace(value.map { |e| convert_value(e) })
        else
          value
        end
      end

1 . value が Hash だったときは、indifferent_access に変換して返します。 というか単にエリアスが貼ってあるだけでした。

  alias nested_under_indifferent_access with_indifferent_access

普通に

value.with_indifferent_access

と書いても良い所をあえてvalue.nested_under_indifferent_accessと書く所、こだわっていますね。

2 . value がArray のとき、

        elsif value.is_a?(Array)
          value.dup.replace(value.map { |e| convert_value(e) })

再帰して返すというお決まりのコードです。ところでなぜ

value.map { |e| convert_value(e) }

ではいけないのでしょうか?

これはおそらく、is_a? は親クラスであっても true になるからだと思います。 value が Array のサブクラスだった場合、`value.map { |e| convert_value(e) } では、サブクラスではなく、Arrayが返ってしまいますからね。

ということで、心臓部はこの2つのメソッドで、後は全て、conver_*を経由するように書き換えているだけでした。例えば、key?convert_key を経由するように書き換えているだけです。

    def key?(key)
      super(convert_key(key))
    end

シンボルでも文字列でもアクセスできる、というのは keyvalue に対して、根本のところで文字列に変換してしまう、という方法で実装されていました。

これさえ理解しおけば、シンボルでも文字列でも扱えるHashが使いこなせますね!

except.rb

Hash の中から、特定の key だけを消したいこと、結構ありますよね?これは有名なので皆さんご存知かと思います。

[12] pry(main)> {a: 1, b: 2}.except(:a, :c)
=> {:b=>2}
  def except(*keys)
    dup.except!(*keys)
  end

  # Replaces the hash without the given keys.
  def except!(*keys)
    keys.each { |key| delete(key) }
    self
  end

keys.rb

Hash の key に対するメソッド群です。

  • stringfy_keys
  def stringify_keys
    dup.stringify_keys!
  end

  # Destructively convert all keys to strings. Same as
  # +stringify_keys+, but modifies +self+.
  def stringify_keys!
    keys.each do |key|
      self[key.to_s] = delete(key)
    end
    self
  end

delete で取り出しながら、key を変換して格納し直すという、 エレガントな入れ替えですね。

  • 逆にシンボル化もできるようです。
  def symbolize_keys
    dup.symbolize_keys!
  end

  # Destructively convert all keys to symbols, as long as they respond
  # to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
  def symbolize_keys!
    keys.each do |key|
      self[(key.to_sym rescue key) || key] = delete(key)
    end
    self
  end

(key.to_sym rescue key)とか、シンプルでかっこいいですね~。

そういえば、to_options とかご存知でした?

  alias_method :to_options,  :symbolize_keys
  alias_method :to_options!, :symbolize_keys!
  • assert_valid_keys で key の確認を便利にできます。
  def assert_valid_keys(*valid_keys)
    valid_keys.flatten!
    each_key do |k|
      raise(ArgumentError, "Unknown key: #{k}") unless valid_keys.include?(k)
    end
  end

reverse_merge.rb

渡した方の hash にマージするメソッドです。

self.reverse_merge(other_hash)ではなく other_hash.merge(self) でいいんじゃないの?

ところが、面白い箇所が1点! merge!( other_hash ){|key,left,right| left } って何ですか?

  def reverse_merge(other_hash)
    other_hash.merge(self)
  end

  # Destructive +reverse_merge+.
  def reverse_merge!(other_hash)
    # right wins if there is no left
    merge!( other_hash ){|key,left,right| left }
  end

  alias_method :reverse_update, :reverse_merge!

同じkeyがあったとき、左側(self)を優先にできるようです。

[24] pry(main)> {a: 1, b: 2}.reverse_merge!({a: 1, c: 3})
=> {:a=>1, :b=>2, :c=>3}
[25] pry(main)> {a: 1, b: 2}.reverse_merge({a: 1, c: 3})
=> {:a=>1, :c=>3, :b=>2}
[26] pry(main)> {a: 1, b: 2}.merge({a: 1, c: 3})
=> {:a=>1, :b=>2, :c=>3}
[27] pry(main)> {a: 1, b: 2}.merge!({a: 1, c: 3})
=> {:a=>1, :b=>2, :c=>3}

破壊型のときは、自分に取り込むよ! ということで自分が優先なんでしょうか? merge に block が渡せるのは、初めて知りました。勉強になります

slice.rb

特定のkey だけを取り出したいこと、よくありますよね?

[15] pry(main)> a = {a: 1, b:2}
=> {:a=>1, :b=>2}
[16] pry(main)> a.slice(:a, :c)
=> {:a=>1}
[17] pry(main)> a
=> {:a=>1, :b=>2}
  def slice(*keys)
    keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
    hash = self.class.new
    keys.each { |k| hash[k] = self[k] if has_key?(k) }
    hash
  end

特定のkeysだけ取り出して、それ以外は返り値で返すとか、delete っぽくでオシャレ!Hashの分割で使えますね。

[18] pry(main)> a.slice!(:a, :c)
=> {:b=>2}
[19] pry(main)> a
=> {:a=>1}
  def slice!(*keys)
    keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
    omit = slice(*self.keys - keys)
    hash = slice(*keys)
    replace(hash)
    omit
  end

逆に特定のkeys だけを返してくれるメソッドもありました。

[20] pry(main)> a = {a: 1, b:2}
=> {:a=>1, :b=>2}
[21] pry(main)> a.extract!(:a, :c)
=> {:a=>1}
[22] pry(main)> a
=> {:b=>2}
  def extract!(*keys)
    result = {}
    keys.each {|key| result[key] = delete(key) }
    result
  end

まとめ

ということで、何か新しい発見はあったでしょうか? 9ファイル19メソッドのHash拡張でした。 with_indifferent_access, deep_dup, except, slice, slice! 辺りは、どんどん使っていきたいと思います。

便利なメソッドがあったら、是非教えて下さい!

宣伝

MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?

大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly

f:id:mgnup:20131118023234p:plain

最速で!最短で!真っ直ぐに!Facebookグループの書き込みを監視する方法!

皆さん、こんにちは!WAというと Web Application ではなく、RPGの方を思い出す MUGENUP の osada です。

いきなりの個人的な話で恐縮ですが、私が初めて書いたWeb Application はPHPでした。当時 Web というと、「SSI で include」したり、「perl で cgi」とか、少し大掛かりだと「Java Applet」?、程度しか知らなかった私には、HTMLの中にプログラムが書けるというのは衝撃的なことでした。PHPの真髄は、すぐに動くことにあるのではないかと思っています。そしてさらに個人的な意見で恐縮ですが、ソースコードで最も価値があるのは動くコードだと思っています。少し学術的に言えば、合目的性ですね。そう言う意味で、PHPは私の好きな言語の一つです。PHP、良い言語ですよね!

はい、ということで今回は、コードの美しさとか全くなし。動けばOK、ということで、Facebook Group への書き込みをウォッチして、新着があったらメールで知らせる ツールを作ってみたいと思います。

え、理由ですか?単に必要だったからです(笑)。

Facebook Developer に登録して、アプリを作成

  1. facebook developer へ登録

    https://developers.facebook.com/

  2. app へ

    f:id:mgnup:20131201224213p:plain

  3. 新しいアプリを作成 ボタン

    f:id:mgnup:20131201224220p:plain

  4. 新しいアプリを作成

    f:id:mgnup:20131201224455p:plain

  5. captcha

    f:id:mgnup:20131201224223p:plain

  6. 承認

    f:id:mgnup:20131201224226p:plain

  7. こんな感じに

    f:id:mgnup:20131201224254p:plain

  8. Graph APIエクスプローラーを使用する

    f:id:mgnup:20131201224209p:plain

  9. アクセストークンを取得

    f:id:mgnup:20131201224230p:plain

  10. user groups にチェック

    f:id:mgnup:20131201224243p:plain

  11. friends groups にチェックして、[Get Access Token]

    f:id:mgnup:20131201224233p:plain

  12. アクセストークンをコピー

    f:id:mgnup:20131201224240p:plain

  13. Facebook にアクセスする koala と メール送信用に mail をインストール

# Gemfile

source 'https://rubygems.org'
gem "koala", "~> 1.8.0rc1"
gem "mail"
group :test, :development do
  gem "pry"
  gem 'pry-byebug'
end

$ bundle

14.書く

watch_facebook.rb

# -*- coding: utf-8 -*-
require "koala"
require "mail"

ACCESS_TOKEN = ARGV[0]
GROUP_ID = ARGV[1]
USER_NAME = ARGV[2]
PASSWORD = ARGV[3]
EMAILS = ARGV[4..-1]
SMTP_SETTINGS = {
  address:   'smtp.gmail.com',
  port:      587,
  domain:    'smtp.gmail.com',
  user_name: USER_NAME,
  password:  PASSWORD
}
FROM = "hoge-report@mugenup.com"
SUBJECT = 'Hoge Report'

begin
  load "config.rb"
rescue LoadError
end

puts "access facebook ..."
graph = Koala::Facebook::API.new(ACCESS_TOKEN)
feeds = graph.get_connections(GROUP_ID, "feed")

puts "reject already feeds ..."
already_feeds_file = open("already_feeds.txt", "a+")
already_read_ids = already_feeds_file.readlines.map(&:chomp)
new_feeds = feeds.reject{|f| already_read_ids.include?(f["id"]) }

exit if new_feeds.size == 0

puts "create messages ..."
messages = new_feeds.map do |f|
  <<-EOS
  #{f["from"]["name"]}
  #{f["message"][0..100]}...
  #{'-' * 50}
  EOS
end
messages << "https://www.facebook.com/groups/#{GROUP_ID}/"

puts "send mail new feeds ..."
mail = Mail.new do
  from    FROM
  to      EMAILS.join(",")
  subject SUBJECT
  body    messages.join("\n")
end
mail.charset = 'utf-8'
mail.delivery_method :smtp, SMTP_SETTINGS
mail.deliver!

puts "store feed_ids (#{new_feeds.size}) ..."
already_feeds_file.puts(new_feeds.map{|f| f["id"]})
already_feeds_file.close

15.使う

$ bundle exec ruby watch_facebook.rb ACCESS_TOKEN GROUP_ID USER_NAME PASSWORD EMAILS ← 適宜読み替えて入力してください。

または、config.rbというのを作って、

f:id:mgnup:20131201230418p:plain

こんな感じに書いてから、

$ bundle exec ruby watch_facebook.rb

16.ACCESS_TOKENの長期化

このままだと、ACCESS_TOKEN の有効期間は1時間です。これを長期化します。

omniauth を入れる(心の)余裕は無いんです!

ということで、下記を参考に、(7)で確認した、app-idとapp-secret、および、アクセストークンを入力して、送信します。

https://graph.facebook.com/oauth/access_token? grant_type=fb_exchange_token& client_id={app-id}&client_secret={app-secret}&fb_exchange_token={short-lived-token} Unable to get a long term access token using facebook graph api - Stack Overflow

17.長期化されたアクセストークンを使う

f:id:mgnup:20131201224250p:plain

こんな感じで、新しく長期化されたアクセストークンが取得できます。 expires5184000 になっていることが確認できます。約2ヶ月、使い続けられます。

18.後は cron で登録するなりなんなり!

あ、cronbundle付きを呼ぶのはちょっと面倒ですが、頑張ってください!

ということで、Facebookのグループを監視するアプリが作れました!

ちょっと解説

設定を引数から受け取る部分

ACCESS_TOKEN = ARGV[0]
GROUP_ID = ARGV[1]
USER_NAME = ARGV[2]
PASSWORD = ARGV[3]
EMAILS = ARGV[4..-1]
SMTP_SETTINGS = {
  address:   'smtp.gmail.com',
  port:      587,
  domain:    'smtp.gmail.com',
  user_name: USER_NAME,
  password:  PASSWORD
}
FROM = "hoge-report@mugenup.com"
SUBJECT = 'Hoge Report'

初めに、一番速いのはなんだろう?と思って、ACCESS_TOKENを直書きしたのですが、これをコミットしてはいけないとゴーストが囁いたので、引数で渡すことにしました。

設定をファイルから受け取る部分

begin
  load "config.rb"
rescue LoadError
end

そうこうしているうちに、毎回引数で渡すのが面倒くさい、ということになり、ファイルに格納することにしました。このconfig.rb.gitignore で対象外にしました。

Facebook のグループからメッセージを獲得する部分

puts "access facebook ..."
graph = Koala::Facebook::API.new(ACCESS_TOKEN)
feeds = graph.get_connections(GROUP_ID, "feed")

特に解説することはありません

既に確認済みのメッセージを削除する部分

puts "reject already feeds ..."
already_feeds_file = open("already_feeds.txt", "a+")
already_read_ids = already_feeds_file.readlines.map(&:chomp)
new_feeds = feeds.reject{|f| already_read_ids.include?(f["id"]) }

exit if new_feeds.size == 0

……特に解説することはありません

メール送信用のメッセージを作る部分

puts "create messages ..."
messages = new_feeds.map do |f|
  <<-EOS
  #{f["from"]["name"]}
  #{f["message"][0..100]}...
  #{'-' * 50}
  EOS
end
messages << "https://www.facebook.com/groups/#{GROUP_ID}/"

......……特に解説することはありません(まさか解説する箇所は出てこないのでしょうか?)

メールを送信する部分

puts "send mail new feeds ..."
mail = Mail.new do
  from    FROM
  to      EMAILS.join(",")
  subject SUBJECT
  body    messages.join("\n")
end
mail.charset = 'utf-8'
mail.delivery_method :smtp, SMTP_SETTINGS
mail.deliver!

当初mailコマンドを使用していたのですが、

connect to aspmx3.googlemail.com[74.125.137.26]:25: Connection refused

ということで、macsmtpからダイレクトに送れなかったため、mailgemを導入しました。 これがなければ、もっとシンプルでもっと速く作れたのに、と思うとちょっと残念です。

さて、Mail ですが、送信相手が複数居る時は、カンマで区切りましょう。

charasetutf-8にしないと、Non US-ASCII detected and no charset defined.という警告がでます。

(説明することが合って良かったぁ〜)

送信したメッセージのIDを記録しておく部分

puts "store feed_ids (#{new_feeds.size}) ..."
already_feeds_file.puts(new_feeds.map{|f| f["id"]})
already_feeds_file.close

特に説明することはありません!

まとめ

以上が、Facebookの特定のグループの新着をメールで送信するツールです。

全くシンプルに、ただ動くことだけを考えて書いたコードですが、いかがだったでしょうか?

ついつい、「DBを使おう」とか「ActiveSuppoだけでも使おう」とか、「関数に分けよう」とか、「クラスを作ろう」とか色々考えてしまうのですが、問題を解決することだけに注目するというのも、また楽しいプログラミングですよね!

そうそうPHPが好きだと言いましたが、単に作成者のRasmus Lerdorf氏が好きなだけかもしれません。彼の発言は一読の価値があると思いますので、宜しければ一度、Rasmus Lerdorf - Google 検索してみてください!

宣伝

MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?

大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly

f:id:mgnup:20131118023234p:plain

あ、WAでビンタしあう方を思い出す方は友達になりましょう。

Startup Live! に登壇してきました

こんにちは、MUGENUP CTOの伊藤です。

12月1日の日曜日にリクルートキャリアアカデミーホールで開催された「Startup Live!」というイベントに、パネルディスカッションのパネラーとして登壇してきました。

f:id:mgnup:20131203162541j:plain

Startup Live!ってなに?

開催概要についてはatndなどに書いてあるので引用します。

【現在参加者100名突破!更に増席50席分はこちら!】12/1(日)開催、話題の急成長スタートアップ8社の経営者が登壇!Start up Live! : ATND

■開催背景 2010年以降ソーシャルメディア、スマートフォンなどの各領域の拡大や日本のベンチャーキャピタルの投資規模拡大により急速に立ち上がるベンチャー企業が増加して参りました。以前に比べ、大企業から起業する人材層や、大企業から数名から数十名のベンチャーの経営幹部となるケースも増加しており、それらの背景から人材大手のリクルートキャリアとシードアーリーステージ企業の支援に特化したベンチャーキャピタルのSkyland Venturesが組んでスタートアップに特化し、成長ベンチャーの社長、CTOと直接会えるリクルーティングセミナーStartup Live!を開催します。当日は成長著しいベンチャー企業の社長、CTOが8社登壇します。

というわけで、「成長ベンチャーのCTO」としてMUGENUPから乗り込んできました。

スタートアップの勢い

クラウドワークス代表吉田さんの講演から始まり、パネルディスカッション<ビジネス>、パネルディスカッション<エンジニア>と各社ともイキイキとした内容となっており、スタートアップならではの面が良く出たイベントでした。

また、イベント翌日の12月2日(月)には登壇企業の内2社が調達のリリースを出しており、その点も改めてスタートアップの勢いを感じる出来事でした。

f:id:mgnup:20131203191925j:plain

パネルディスカッションでは主に

  • 開発体制はどうなっているか
  • スタートアップのやりがい
  • 今後エンジニアはどうすればいいのか

といった内容について話してきました。

開発体制はやはり気になるところで結構イベントの時などでも話題になりますよね、 せっかくなので弊社の開発体制についてここで少しご紹介します。

MUGENUPシステムチーム開発体制

MUGENUPではクラウドのクリエイターさんと社内の人間をつなぐ「MUGENUP WORK STATION」という システムで全ての制作物を管理しており、システムチームはその開発、運用を行っています。

チームで利用しているサービス、ツールは主に以下のものになります。

  • PivotalTracker ストーリーの管理
  • GitHub コードの管理、プルリクのレビューなど
  • AWS サーバーサイドは全てAWSです
  • ChatWork チーム内や社内の人との情報共有
  • Skype チーム内の情報共有
  • Qiita:Team チーム内のノウハウの共有など
  • Toggl 見積もりの実測値計測用に

基本的に週に一回のタイミングでリリースを行うので、現在は週一で振り返りを行っています。

f:id:mgnup:20131203164754j:plain

(イーゼルパッドと付箋を用いてぺたぺたやっています)

話は戻って

さて、そんなこんなでベンチャー色が出まくったイベントだったんですが、当日はLINE株式会社の森川社長もいらっしゃっていてブログに書かれています。

ベンチャー企業のリクルートイベントに行ってきました : LINE株式会社 森川社長ブログ

私自身今後はイベント等にどんどん出て行きたいと考えているので、MUGENUPのこと、MUGENUPシステムチームのことで話したいことがある人はぜひお声をかけてもらえればと思います。

宣伝

MUGENUP では、アジャイルな開発に興味津々なエンジニアを募集中です。 無限流開発、ご一緒しませんか?

大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly

f:id:mgnup:20131118023234p:plain

should を捨て expect を使うようになった、たった一つの理由

皆さんこんにちは。スーパーのビニール袋は濡れタオルを触ってから開ける方、MUGENUP の osada です。 今回はexpect について調査したことを書きたいと思います。 ターゲットは、rspecが上手く書けなくて、気が付くと3時間もテストを書いている、というようなrspec初心者です(実話)。

中間管理職 expect さん

では、本題のexpectです。まずは問1から。5秒で答えてください。

describe Person do
  it "should be a instance" do
    expect{Person.new}.to be_a(Person)
  end

  it "should be a instance" do
    expect(Person.new).to be_a(Person)
  end
end 

問1

  1. 両方テストが通る
  2. 上のテストが通らない
  3. 下のテストが通らない
  4. 両方通らない









できました?正解は

解答: 2. 上のテストが通らない

  1) Person should be a instance
     Failure/Error: expect{Person.new}.to be_a Person
       expected #<Proc:0x007fbcf91a75a8@/Users/osada/projects/ruby_test/rspec_test/spec/person_spec.rb:9> to be a kind of Person
     # ./spec/person_spec.rb:9:in `block (2 levels) in <top (required)>' 

では、問2

上のテストが通らない理由を説明してください

違いは、{}()です。しかし前回のchange マッチャで使っていたのは{}のブロックでした。今回はなぜダメなんでしょうか?

調べていきましょう。

ソースと共にあらんことを

ソースコードを読んで分かるように書くべきである、という考え方にあるように、Rubyist は常にソースと共にあるようです。 では、expect の深淵に足を踏み入れたいと思います。

私の環境では、ruby/2.0.0/gems/rspec-expectations-2.14.2/lib/rspec/expectations/syntax.rb が入り口のようです。

expect と to

          def expect(*target, &target_block)
            target << target_block if block_given?
            raise ArgumentError.new("You must pass an argument or a block to #expect but not both.") unless target.size == 1
            ::RSpec::Expectations::ExpectationTarget.new(target.first)
          end

block のときは、targetに追加してから、target.firstを委譲しています。ということは、引数とブロックを同時に渡せるのか?、とおもいきや、target.size==1以外受け付けないので、ちょっと不思議な構造です。

::RSpec::Expectations::ExpectationTargetは同じ階層のexpectation_target.rbにあります。

    # @example
    #   expect(something) # => ExpectationTarget wrapping something 

    class ExpectationTarget

      # @api private
      def initialize(target)
        @target = target
      end 

とあるように、expectはただのラッパーのようです。 そして target はそのまま @targetに代入されています。ブロックだからといって、特別扱いはしていませんね。

では、続いて toを見てみます。

      def to(matcher=nil, message=nil, &block)
        prevent_operator_matchers(:to, matcher)
        RSpec::Expectations::PositiveExpectationHandler.handle_matcher(@target, matcher, message, &block)
      end 

引数を見て驚きました。toにはmessageblockが渡せるのですね。そしてその先は、PositiveExpectationHandlerに委譲しているようです。

なんと expect がしているのはこれだけです!シンプルですね。 この先はhandlerを読まなければなりません。

PositiveExpectationHandler

PositiveExpectationHandler があるのは、同じ階層の handler.rb です。

ruby/2.0.0/gems/rspec-expectations-2.14.2/lib/rspec/expectations/handler.rb

    class PositiveExpectationHandler < ExpectationHandler

      def self.handle_matcher(actual, matcher, message=nil, &block)
        check_message(message)
        ::RSpec::Matchers.last_should = :should
        ::RSpec::Matchers.last_matcher = matcher
        return ::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher.new(actual) if matcher.nil?

いきなり困りました。 コロン2つで始まる式なんてありましたっけ? そのことについては後述することにして先に進めます。

        match = matcher.matches?(actual, &block)
        return match if match

ここが判定の部分です。 前回 changeマッチャ で見ましたが、matchermatches? メソッドを呼びだしています。 こうゆうの(↓)ですね。 to からくる block を渡せる matcher と 渡せない matcher があるようです。change は渡せない方だったのですね。

         def matches?(event_proc)
          raise_block_syntax_error if block_given?

          @actual_before = evaluate_value_proc
          event_proc.call
          @actual_after = evaluate_value_proc

        (!change_expected? || changed?) && matches_before? && matches_after? && matches_expected_delta? && matches_min? && matches_max?
        end

結局のところ、expectで渡したtarget はマッチャーの所まで、何の加工もなくそのまま委譲されるということが分かりますね。そしてbe_aマッチャ(実体はbe_kind_ofマッチャ) は、こんな感じです。

      class BeAKindOf < BaseMatcher
        def match(expected, actual)
          actual.kind_of? expected
        end
      end

やっと{}で書くと失敗する理由がわかった気がします。 つまり、{Person.new}.kind_of? PersonProc.kind_of? Personですので、ProcオブジェクトはPersonインスタンスではない、といことで失敗しているのでした。===で評価しているマッチャであれば、成功するに違いありません。

ということで、expect に何を渡すかは、マッチャによるという身も蓋も無い話になってしまいましたが、ここを意識して書いていくことで、もっとスラスラと rspec が書けると良いなと思っています。

message は失敗したときの表示

せっかくなので続きを見ていきましょう。

match で true になったら、return するので、以降は match しなかった時の処理のようです。

        message = message.call if message.respond_to?(:call)

        message ||= matcher.respond_to?(:failure_message_for_should) ?
                    matcher.failure_message_for_should :
                    matcher.failure_message

to の第2引数、message はここで使われるようです。 オリジナルのエラーメッセージを返すことができるんですね。試してみます。

  it "should be a instance" do
    expect{Person.new}.to be_a(Person), "通らないヨーダ"
  end
  1) Person should be a instance
     Failure/Error: expect{Person.new}.to be_a(Person), "通らないヨーダ"
       通らないヨーダ
     # ./spec/person_spec.rb:10:in `block (2 levels) in <top (required)>' 

おお、本当に変わりました。

指定しないときはいつものエラーメッセージですが、 それらは matcher が返していたようです。

message.callMethodクラスのことのようですが、奥が深いのでスルーします(えっ)。 エラーメッセージを返すメソッドを作成して、 それを rspec の引数に渡すような使い方を想定しているのかもしれません。

diffable

ここで最後です。diffable? とは何でしょうか?

        if matcher.respond_to?(:diffable?) && matcher.diffable?
          ::RSpec::Expectations.fail_with message, matcher.expected, matcher.actual
        else
          ::RSpec::Expectations.fail_with message
        end
      end
    end

これを持っている matcher を探してみると、base_matcher.rbfalse, eq.rb, eql.rb, equal.rbtrue、およびinclude.rb はメソッド呼び出しがありました。

> rspec-expectations-2.14.2/lib/rspec/matchers/built_in/base_matcher.rb
      def diffable?
         false
      end 

> rspec-expectations-2.14.2/lib/rspec/matchers/built_in/eq.rb
        def diffable?; true; end 

> rspec-expectations-2.14.2/lib/rspec/matchers/built_in/include.rb
        def diffable?
          # Matchers do not diff well, since diff uses their inspect
          # output, which includes their instance variables and such.
          @expected.none? { |e| RSpec::Matchers.is_a_matcher?(e) }
        end 

比較可能な場合において、期待値と実測値を表示するのに使うようですね。

まとめ

今回の調査でわかったのは下記の2点です。

  1. expectmatcherにそのまま委譲する。expect{}.toで書けるのは、matcher 側がブロックを評価しているときのみ
  2. toにはmessage&blockが渡せる

ということで、matcher を知ることが、expect を書けるようになる条件なんですね。 基本の matcher はBuilt in matchers - RSpec Expectations - RSpec - Relishにありましたので、勉強していきたいと思います。

誤差をOKにするbe_with_in、範囲を比較する cover、他にも色々あるので、便利に使っていきたいです。 初めて知ったマッチャについてご紹介だけしておきます。

  • be_with_in 誤差をOKにするマッチャ

    should be_within(0.5).of(27.9)

  • exist exist? を判定するマッチャ。

    expect(obj).to exist

  • match 正規表現のマッチャ =~と同じ

    expect("a string").to match(/str/)

  • respond_to クラスのメソッドが呼び出せるかどうか。しかも引数の呼び出し個数までマッチできる!

    expect([1, 2, 3]).to respond_to(:take).with(1).argument

  • satisfy ブロックが渡せるマッチャ。何でもできちゃう。

    expect(10).to satisfy { |v| v % 5 == 0 }

  • throw throw で渡ってきたシンボルを確認するマッチャ

    expect { throw :foo }.to throw_symbol(:foo)

  • yield 一言では説明できないのでパス! yield matchers - Built in matchers - RSpec Expectations - RSpec - Relish
  • cover 範囲をマッチ。というか、range に cover なんてメソッドあったんですね。

    expect(1..10).to cover(5)

  • start_with, end_with。先頭か末尾をマッチ。というか、ファイル名がstart_and_end_with.rbなんですが

    expect("this string").to end_with "string"

使えそうなもの、使えなさそうなもの。色々あって、楽しいですね。 これからも良いrspecを書いていきたいと思います。

あ、表題shouldを捨てた理由、が書いてありませんでしたね。

  1. shouldself を対象にした handler 呼び出しでしかないから。
        syntax_host.module_eval do
          def should(matcher=nil, message=nil, &block)
            ::RSpec::Expectations::PositiveExpectationHandler.handle_matcher(self, matcher, message, &block)
          end

やってること同じじゃん、ということで、これなら expect で書いたほうがクラスを開く必要がない分が良いな、と思った次第です。文字数増えるのはイヤなんですけどね〜。

では皆さん。良いspec ライフをお送り下さい。

宣伝

MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?

大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly

f:id:mgnup:20131118023234p:plain

(補講) ::って何よ?

さて、ちょっと後に回していた、::についての調査結果です。

        ::RSpec::Matchers.last_should = :should

あるクラスまたはモジュールで定義された定数を外部から参照する ためには::演算子を用います。 またObjectクラスで 定義されている定数(トップレベルの定数と言う)を確実に参照する ためには左辺無しの::演算子が使えます。 親クラスとネストの外側のクラスで同名の定数が定義されているとネストの外 側の定数の方を先に参照します。 http://docs.ruby-lang.org/ja/2.0.0/doc/spec=2fvariables.html#const

# -*- coding: utf-8 -*-
CONSTANT = "トップの定数だよ"

class Person
  CONSTANT = "内部の定数だよ"
  def top_level_constant
    ::CONSTANT
  end

  def my_class_constant
    CONSTANT
  end
end
describe Person do
  it do
    expect(Person.new.my_class_constant).to eq "内部の定数だよ"
  end

  it do
    expect(Person.new.top_level_constant).to eq "トップの定数だよ"
  end
end

(補講2) ボクの考えた最強のマッチャ

最後に皆さんに課題です。3秒で選んでください。

  it "should be true" do
    expect{Person.new}.to be_true
  end 
  1. 通る
  2. 通らない

探検!Changeマッチャ

皆さんこんにちは! 株式会社 MUGENUP 開発部の osada です。 弊社 で使っているフレームワークRuby on Rails ということで、 テストで使用する change マッチャ について調べたことを書きたいと思います。 ターゲットは change でテストを書きたい rails 初心者の方です。 かくいう私もrails歴は半年もなく、初心者の皆さんと一緒に勉強出来ればと思います。

ですがその前に、MUGENUP 開発部って何をやっているの?ということで、 簡単にご紹介したいと思います。

MUGENUP とは?

株式会社 MUGENUP は、イラストを必要とする企業さんから発注を受けて、クリエイターさん達とアートディレクターが連携して、イラストを作成しお届けしています。 それらの各工程「受注、工程分割、担当割り当て、イラストの修正や相談」などの全てを MUGENUP WORK STATION というWEBシステムで管理しています。掲示板、画像投稿に変換、リアルタイム通知、スケジュール管理などなど、機能てんこ盛りなWEBシステムです。これのお陰で、日本中、世界中のクリエイターさん達が、いつ、どこにいても一緒に働けるという、クラウドならではの共同作業を実現しています。

f:id:mgnup:20131117233603p:plain

開発部では、これら全てを、Ruby on Rails で開発しています。Rails 3, Ruby 2.0 (!) で動いています。

Change Matcher のブロックは2度評価される

さて、そんな MUGENUP では今 change 熱 が流行っています。現在、開発を株式会社KRAYさんにご協力いたただいているのですが、大変勉強になることばかりで、その一つが 良いテストを書く ということです。

綺麗なテストを書くためには、should...== を羅列するのではなく、change().from().to()を使おう、ということになるのですが、どう書いたら良いかわからないことが多かったです。

例えば、ユーザーの email を update すると、新しいメールアドレスになるということを下記のように書きました。

        expect{
          put :update, {id: @user.id, user: {email: new_email}}
        }.to change{@user.reload.email}.from(@user.email).to(new_email)

これとは別に下記の書き方もできると思います。

        expect{
          put :update, {id: @user.id, user: {email: new_email}}
          @user.reload
        }.to change(@user, :email).from(@user.email).to(new_email)

どんな書き方が良いのでしょうか?そもそも、なぜこれが動くのでしょうか?

ドキュメントがないならソースを読めばいいじゃない ということで、ソースを当たることにしました。

ソースは私の環境ではvendor/bundle/ruby/2.0.0/gems/rspec-expectations-2.14.2/lib/rspec/matchers/built_in/change.rb にありました。

インスタンス化 フェーズ

      class Change
        def initialize(receiver=nil, message=nil, &block)
          @message = message
          @value_proc = block || lambda {receiver.__send__(message)}
          @expected_after = @expected_before = @minimum = @maximum = @expected_delta = nil
          @eval_before = @eval_after = false
        end

インスタンス化はここです。ブロックを渡した時はそのままが評価されます。change(@user, :email) のように、receiverを渡した時は、lambda化されて、__send__ で渡されるようです。blockが先に来ていますので、change には blockを渡すのが王道なんでしょうか。

matcher フェーズ

        def matches?(event_proc)
          raise_block_syntax_error if block_given?

          @actual_before = evaluate_value_proc
          event_proc.call
          @actual_after = evaluate_value_proc

          (!change_expected? || changed?) && matches_before? && matches_after? && matches_expected_delta? && matches_min? && matches_max?
        end

        def evaluate_value_proc
          case val = @value_proc.call
          when Enumerable, String
            val.dup
          else
            val
          end
        end

match 判定 はこれです。インスタンス化で渡されたvalue_procが、event_proc の前後で発動し、評価されていることがわかります。

また、@value_procの返り値が Enumerable, String だった場合には、dup で返しているのも気が利いていますね。こうしないと破壊的操作がされたときに、追従してしまうことになり、テストが成立しないからだと考えられます。さらに言えばcase ... whenでは valEnumerable, Stringクラスのインスタンスかどうかを判定できることを利用した美しいコードです。

from と to と by と

match 判定の部分を見て行きましょう。

          (!change_expected? || changed?) && matches_before? && matches_after? && matches_expected_delta? && matches_min? && matches_max?

changedは実行前後の値の変化を、change_expectedby メソッドが呼ばれているかどうかで判定しています。

        def changed?
          @actual_before != @actual_after
        end

        def change_expected?
          @expected_delta != 0
        end

        def by(expected_delta)
          @expected_delta = expected_delta
          self
        end

matches_before?from メソッドが呼ばれた時に @eval_beforeフラグがセットされ、fromに渡した値と、@value_procの値が比較されます。

        def matches_before?
          @eval_before ? expected_matches_actual?(@expected_before, @actual_before) : true
        end

        def from (before)
          @eval_before = true
          @expected_before = before
          self
        end

matches_after? はその逆で、toメソッドが呼ばれた時に同じ挙動となります。

matches_expected_delta?byメソッドの判定で、実行前の実測値と差分の期待値が、実行後の実測値と同じかどうかを判定していました。

        def matches_expected_delta?
          @expected_delta ? (@actual_before + @expected_delta == @actual_after) : true
        end

これがおよそ、match 判定の部分です。 fromtobyも、フラグをセットし、実行前後の値の比較という挙動をしています。 およそ change マッチャの挙動を理解できたのではないでしょうか。 シンプルで素晴らしい構造だと思います。

そしてソースを読んだことで、知らなかったメソッドも知ることが出来ました。 それがby_at_leastby_at_mostです。

by_at_least と by_at_most

by_at_leastby_at_mostは下記となります。実行後と実行前の差分が、(少なくとも|多くとも) x である、ということのようです。いつ使うんでしょうか?ランダム要素があるときは、Hoge.stub(:rand){} で確定させて返してしまうのですが、その用途ではないのでしょうか。機会があれば、使ってみたいと思います。

        def by_at_least(minimum)
          @minimum = minimum
          self
        end

        def by_at_most(maximum)
          @maximum = maximum
          self
        end

        def matches_min?
          @minimum ? (@actual_after - @actual_before >= @minimum) : true
        end

        def matches_max?
          @maximum ? (@actual_after - @actual_before <= @maximum) : true
        end

まとめ

さて、長々と、Change class を見てきましたが、初めの疑問、change のブロックの中に reload を書くか、書かないか、はどちらが良いのでしょうか?

changeブロックが2回評価されることを考えると、1度目のreloadは無意味なのにDBアクセスを起こすので、changeの外にだすべきなのかな、と個人的に思います。ただ時間を計測したところそれほど差異が無かったため、もう少し突き詰めたいところです。

あと、今更で大変恐縮なのですが、changeマッチャについて知りたい人は、かの有名な Rubyist Magazine - スはスペックのス 【第 1 回】 RSpec の概要と、RSpec on Rails (モデル編)を読んでください。これだけ読めば、テストが書けるようになります!

では、最後に宣伝を。 MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?

大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly

f:id:mgnup:20131118023234p:plain