主夫ときどきプログラマ

データベース、Webエンジニアリング、コミュニティ、etc

Rubyでメタプログラミング

勉強会でRubyについて話す機会があったのでその資料を公開します。
勉強会についてのエントリはこちら。 スプーキーズの勉強会イベント一般公開します。 - スプーキーズの中の人。

メタプログラミングとは?

コードを記述するコードを記述すること

メタプログラミング (metaprogramming) とはプログラミング技法の一種で、ロジックを直接コーディングするのではなく、あるパターンをもったロジックを生成する高位ロジックによってプログラミングを行う方法、またその高位ロジックを定義する方法のこと。主に対象言語に埋め込まれたマクロ言語によって行われる。

by wikipedia

It's Magic!

Rubyを使ってDBにアクセスするプログラムを書いてみよう。 今回書くのはActiveRecordを使ってmoviesテーブルにアクセスするクラスだ。

class Movie < ActiveRecord::Base
end

Wow, たったこれだけ!

movie = Movie.new
movei.title = '燃えよドラゴンズ!'
movie.title                  # => '燃えよドラゴンズ!'
movie.save

ActiveRecord はテーブルを知ってるの?

知りません

Movie#title()Movie#title=() を呼び出しているが、これらのソースコードはどこにもない。 どこにも定義されてないとするとどうやって存在するんだい?

これこそメタプログラミングがやってくれるんだ!!

実行時にメソッドを定義している

ActiveRecord は実行時にクラス名を調べ簡単な規約を適用します。 例えば Movie クラスであれば movies テーブルにマッピングします。 そしてデータベーススキーマを読み取り title カラムがあることを見つけ、この属性のアクセッサメソッドを定義しています。

プログラムの実行中にこっそりと定義してるのだ!!

メタプログラミングするには

魔法のようなメタプログラミングをするには魔術書が必要!

オープンクラス

文字列からアルファベットと数字以外を除外したい。

def to_alphanumeric(s)
    s.gsub /[^\w\s]/, ''
end

to_alphanumeric("#hello, it's magic number 3*?")     # => "hello its magic number 3"

でもこのメソッドはオブジェクト指向的じゃないよね?

文字列自身に変換してもらおう

class String
  def to_alphanumeric
    gsub /[^\w\s]/, ''
  end
end

"#hello, it's magic number 3*?".to_alphanumeric()    # => "hello its magic number 3"

いつだってクラスを再オープンできる

class A
  def x; 'x'; end
end

class A
  def y; 'y'; end
end

obj = A.new
obj.x       # => 'x'
obj.y       # => 'y'

class キーワードはクラス宣言というよりはスコープ演算子のようなもので、class を使ってクラスコンテキストに行きそこでメソッドを定義する。つまりいつでもクラスを再オープンしてその場で修正ができる。

モンキーパッチ

同じ要領で配列要素を置換するメソッドを定義しよう

def replace(array, from, to)
  array.each_with_index do |e, i|
    array[i] = to if e == from
  end
end
# Arrayクラスに再定義
class Array
  def replace(from, to)
    each_with_index do |e, i|
      self[i] = to if e == from
  end
end

これをプロダクトのコードでやったらおかしなことになってしまうぞ!

既存のメソッドを上書きしてしまった

> [].methods.grep /^re/
 => [:reverse_each, :reverse, :reverse!, :reject, :reject!, :replace, .....

独自の replace() メソッドでうっかり元の replace() メソッドを上書きしてしまった。 こうしたクラスへの安易なパッチに否定的な人には蔑称で モンキーパッチ って言われちゃうぞ!

これが オープンクラスのダークサイドだ。

メソッドを知る

Rubyでは動的なメソッド呼び出しやメソッド定義が可能だ。

Object#send() メソッド

メソッドを呼び出すには通常ドット記法を使う。

class Person
  def saba(my_age)
    my_age - 5
  end
end

obj = Person.new
obj.saba(35)           # => 30

Object#send() メソッドでも呼び出すことができる。

obj.send(:saba, 45)    # => 40

動的ディスパッチ

obj.saba(35)           # => 30
obj.send(:saba, 45)    # => 40

どちらのコードも saba() を呼び出してるが、後者は send() を使っている。これはメソッドで第1引数はメソッド名、その他の引数(とブロック引数)はそのままメソッドに渡される。

つまりメソッドを実行するメソッドというわけだ。

呼び出したいメソッド名が通常の引数になるから実行時に呼び出すメソッドを直前に決められる。これを動的ディスパッチと呼ぶ。

メソッドを動的に定義する

Module#define_method() を使えば、メソッドをその場で定義することができる。メソッド名とブロックを渡す必要があり、ブロックがメソッドの本体となる。

class Spookies
  define_method :muscle do |name|
    "#{name}, さぁ今日も筋トレだ!"
  end
end

spoo = Spookies.new
spoo.muscle("アッシー")        # => アッシー, さぁ今日も筋トレだ!

動的メソッド

通常defキーワードを使って定義するメソッドをModule#define_method()で定義している。これはメソッドを定義するメソッドだ。

define_method()Spookiesのなかで実行され、muscle()Spookiesインスタンスメソッドとして定義される。実行時にメソッドを定義するこのテクニックは動的メソッドと呼ばれる。

エイリアス

エイリアスのメソッドもまたメソッドだ

def deserves_a_look?(book)
  amazon = Amazon.new
  amazon.reviews_of(book).size > 20
end  

例外処理が考慮されてないメソッドをなんとかしたい。 こんな時はこのメソッドをラップして、機能を追加して、全てのクライアントが新しく追加した機能を自動的に使えるようにしたらいい。

メソッドエイリアス

alias キーワードを使えば、 Rubyのメソッドにエイリアス(別名)をつけられる。

class Pizza
  def cheese
    'cheese !'
  end
  alias :formaggio :cheese


  def tomato; 'tomato!'; end
  alias pomodori tomato
end

p = Pizza.new
p.cheese         # => "cheese !"
p.formaggio      # => "cheese !"
p.pomodori       # => "tomato!"
  • aliasはキーワード
  • 新しい名前が前、古い名前が後でカンマ不要
  • シンボルでもいいし素の名前でもいい
  • メソッドではない
  • 同じ動きをするModule#alias_method()メソッド
  • String#size()String#lengthエイリアス

メソッドを alias して再定義すると何が起きるだろうか?

class String
  alias :real_length :length

  def length
    real_length > 5 ? 'long' : 'short'
  end
end

"I'm so hungry".length            # => "long"
"I'm so happy!".real_length       # => 13

このコードはString#length()を再定義している。しかしエイリアスは元のメソッドを参照している。 メソッドの再定義とは、元のメソッドを変更するのではなく、新しいメソッドを定義して、元のメソッドの名前をつけているわけだ。

アラウンドエイリアス

class Fixnum
  alias :old_plus :+

  def +(value)
    self.old_plus(value).old_plus(1)
  end
end

3 + 5        # => 9
4 + 10       # => 15

新しい +()old_plus()の周りをラップしている。 これをアラウンドエイリアスと呼ぶ。

  1. メソッドにエイリアスをつける
  2. 新しいメソッドを定義する
  3. 新しいメソッドから古いメソッドを呼び出す

でもどんなときに使ったらいいの?

事例紹介

HTMLページを量産したい

  • 飲食チェーンの広告ランディングページ
  • 100店舗すべて別ページを作る
  • 基本デザインは同じ
  • 店舗固有のデータ(店名や住所など)はCSVで管理

テンプレートとデータを読み込んで各店舗のページを出力するようなツールが欲しい

イメージはこんな感じ

f:id:masayuki14:20160428121521p:plain

まずはCSVを読み込もう

新米プログラマの例

store_id store_name address
102 おばけベーカリー 新宿店 新宿区新宿2丁目・・・
103 おばけベーカリー 渋谷店 渋谷区宇田川町・・・
110 おばけベーカリー 池袋店 豊島区東池袋1丁目・・・
line_number, rows = [ 0, {} ]
CSV.foreach('data.csv') do |line|
  line_number += 1

  rows << {
    store_id: line[0],
    store_name: line[1],
    address: line[2],
  }
end

これってイケてないよね

  • データの追加や順番が変わるたびにコードの修正が必要
  • store_id などの項目名が生かせてない

こんな感じにしたい!

row = SupponRecord.new(line)

row.store_id       # => 102
row.store_name     # => おばけベーカリー 新宿店
row.address        # => 新宿区新宿2丁目・・・

オープンクラスと動的メソッド

CSVファイルの1行目(項目名)を使ってアクセッサを動的に定義してやればいい!

class SpooRecord

  ### オープンクラスの同類
  # 特定のインスタンスを再オープンして定義を追加します
  # この中で定義されたメソッドはクラスメソッドになります
  class << self

      ### 属性の定義
      # attributes = ['store_id', 'store_name'] の場合
      # store_id, store_id=, store_name, store_name= のアクセッサを定義する
      def define_attributes(attributes)

        attributes.each do |attr|
          attr_accessor attr # attrへのアクセッサを定義
        end
    end
  end
end

irb で実行してみよう

2.2.1 :020 >   SpooRecord.define_attributes( ['id', 'name', 'address'] )
 => ["id", "name", "address"]
2.2.1 :021 > r = SpooRecord.new
 => #<SpooRecord:0x007ffcc1046378>
2.2.1 :022 > r.id = 100
 => 100
2.2.1 :023 > r.name = 'Spookies'
 => "Spookies"
2.2.1 :024 > r.address = '京都市'
 => "京都市"
2.2.1 :025 > p r
#<SpooRecord:0x007ffcc1046378 @id=100, @name="Spookies", @address="京都市">
 => #<SpooRecord:0x007ffcc1046378 @id=100, @name="Spookies", @address="京都市">
2.2.1 :026 >

動的ディスパッチで値を設定する

2行目以降のデータを対応する項目値として設定する

class SpooRecord
  class << self

    # indexと項目名の対応を保持, 1 => store_id
    # クラスにアクセッサが定義される
    # SpooRecord.index_to_attr にアクセスできる
    attr_accessor :index_to_attr 

    def define_attributes(attributes)
      @index_to_attr ||= {}

      attributes.each_with_index do |attr, index|
        attr_accessor attr # attrへのアクセッサ
        @index_to_attr[index] = attr # indexと項目名の対応
      end

    end
  end

  # コンストラクタ
  def initialize(data)
    data.each_with_index do |value, index|
      attr = self.class.index_to_attr[index] # クラスメソッドの `index_to_attr` で読込
      self.send("#{attr}=", value) # 動的ディスパッチ!!
    end
  end
end

irb で実行してみよう

2.2.1 :023 >   SpooRecord.define_attributes( ['id', 'name', 'address'] )
 => ["id", "name", "address"]
2.2.1 :024 > r = SpooRecord.new( [100, 'Spookies', '京都市'] )
 => #<SpooRecord:0x007fd9c10945c8 @id=100, @name="Spookies", @address="京都市">
2.2.1 :025 > p r
#<SpooRecord:0x007fd9c10945c8 @id=100, @name="Spookies", @address="京都市">
 => #<SpooRecord:0x007fd9c10945c8 @id=100, @name="Spookies", @address="京都市">

出力は ERB へバインド

例えばテンプレートがERBだったらそのままバインドしちゃいましょう

<body>
  <ul>
    <li>店舗: <%= store_name %></li>
    <li>住所: <%= address %></li>
  </ul>
</body>
class SpooRecord

  ### テンプレートにデータをバインドしてブロックに返す
  def bind_erb(template_path)
    File.open(template_path, 'r') do |file|
      yield(ERB.new(file.read).result(binding))
    end
  end
end

SpooRecord を使ってみた

line_number = 0

CSV.foreach('data.csv') do |line|

  if line_number == 1
    SupponRecord.define_attributes( line ) 
    next
  end

  row = SupponRecord.new(line)
  row.bind_erb( 'template.erb') do |binded|
    File.open("#{row.store_id}/index.html", 'w') { |file| file.write(binded) }
  end
end

他にもいろいろ機能をついかしていくと。。。

https://gist.github.com/masayuki14/9d1e2aace3b75369c6ec5d15cdd6a642

メタプログラミングRuby

2016年の目標

去年やったことをめずらしく書きだしたのでせっかくだから今年の目標も書いておく

masayuki14.hatenablog.com

今年の目標

Swift2

swift2を試す。iOsネイティブのアプリを作れるようになろう。年内にクソゲーがリリースできたらバンザイ\(^o^)/

Redux

Reactで作ったアプリにReduxを導入する。

Hubot

なんかつくる。くだらないものからやっていこう。みさわの画像出すとか。すでにありそうだけど。

Mackerel

サーバーにMackerel入れてみる。APIコールして独自のメトリクスとりたい。

ブログ

月に1回はブログを書く。ちゃんとアウトプットしよう。

IPA試験

IPAの試験を受けてなんか合格したい。DBとネットワークあたりか。春の試験は受付中。

OSSコミッタ

になろう。規模の大小は問わずGitHubでPullRequestを送ってみる。

コミュニティ活動

勉強会とか参加してみよう。

2015年にやったこと

今年は意外といろいろ新しいことやったので覚えてる範囲でかきだし。

今年やったこと

zsh

勉強して設定ファイル作った。いろんな小さな問題が解消して嬉しい。pecoとhistory連携がすこぶる便利。

Chef, Vagrant

開発・本番環境のセットアップを自動化。一部手動が残ってしまったけど悪くなかった。 これを始めてからちょっとしたものもすべてVMを使うようになった。レシピも再利用できてよい。 今後も継続利用するだろう。

Redis

PHPのWEBアプリケーションから利用。環境も簡単にセットアップできるし利用も簡単であった。SortedSetやLuaスクリプトなど特徴的な機能を使ってないのでRedis知ってるよレベルとさほど変わらない。今後使う予定がないので残念。

React, Material-UI, ECMAScript6

いちばんしんどかったやつ。フロントエンド開発についていろいろ知ることができた。gulp, babelなど周辺にはnodeの闇が広がっていてReactでアプリケーションを作るという目的を見失いそうだった。 結果fluxは使わずにフロントのSPA(single page application)をReact-Router,Material-UIで開発してリリースし運用してる状態。ES6スタイルでコーティングしてgulpでbuildする感じ。 思想やアーキテクチャは文句なく素晴らしいが、変化が速いのでバージョンアップしたら動かなかったり、ブラウザも選ぶのでその辺は考慮が必要だろう。

Sinatra, ActiveRecord

ReactAppのサーバーサイドで利用。APIとして実装しすべてをJSONで通信するようにした。フロントとサーバーで責任の範囲を分割でき、コードの可読性はかなり高くなった印象。 数年ぶりに使ったActiveRecordも相変わらず便利。DB設計やネーミングがそのままコードに出てくるから1つのカラム(xxx_statusとか)で状態を管理するのは良くなさそう。

Perl

年間通して使った言語。いまだにリファレンスまわりがよく分からないけど grep, sort, map はわりと使いこなしている。 なぜかしっかり学ぼうというモチベーションが上がらない言語。そろそろ飽きてきた。

Hubot

ごく最近動く環境をいれたのでHelloWorldレベル。slackで動く環境はすでにあるから何をつくろーか、と思うものの何も思い浮かばない。

朝型生活 再入門

毎朝5時起きの朝型生活を再開して2カ月が経とうとしているのでそれについて書きます。
"早起きは三文の得" と言うように、朝型生活をとりいれると様々な良いことがあります。
さあ、あなたも朝型生活を始めましょう。

朝型生活の始め方

1.冬は避ける

冬は動物の活動が低下します。人間も例外ではありません。
朝は寒さのあまり布団から出られません。おまけに外も真っ暗です。
朝型生活を始めるなら冬は避けましょう。
今は夏です。チャンスです。

2.早く寝る

早寝早起きが基本です。21時には寝床に着きましょう。小さいお子さんがいる方は一緒にねましょう。
早く起きる為には十分な睡眠が必要です。

3.起床後の予定を決めておく

起きてからやることが決まっていないと、起きてもまた寝てしまいます。
寝る前にやることを決めておきましょう。
私の場合はすぐ仕事を始めます。

3つ達成できれば成功したも同然


上記の3つが達成できれば、朝型生活に成功したも同然です。
それでもまだ起きられない人はもう少し頑張ってみましょう。

4.目覚まし時計を変える

巷では10万円の目覚まし時計の評判が良いので試してみる価値はありそうです。
http://r25.yahoo.co.jp/fushigi/report/?id=20130327-00028960-r25
そんなお金出せないよ、という方は "Sleep Cycle" というアプリを試してみましょう。
眠りの状態を解析し、設定時刻の30分前から間で最適なタイミングにアラームを鳴らせてくれます。
http://www.sleepcycle.com/
私も使っています。

5.朝日を浴びる

起きたら朝日を浴びましょう。外へ出ると少し肌寒いですが気持ちいです。
これができれば成功している証拠です。

朝型生活はいいことがいっぱい


朝型生活とは1日の開始時刻を早めることです。
活動時間はかわりません。
つまり活動する時間帯をずらだけです。
たったそれだけでたくさんのメリットを享受することができます。

健康になりストレスが減る

毎日規則正しく早く寝て早く起きることで健康になります。
睡眠をしっかり取ることで疲れがとれ、朝日を浴びることで体内時計もリセットされます。
肌ツヤも良くなり見た目にも若さがみなぎってきます。

生産性があがる

脳の働きは午前中にピークに達し午後には低下していことが実証されています。
雑念のないクリアな思考ができ、よいアイディアも浮かびます。
誠実さも午後になると欠如していく傾向にあるという研究結果もでています。

邪魔が入らない

日中はチャット、メール、インスタントメッセージ、SNSなどからの
様々な通知が集中を阻害しがちですが、
早朝はそういった邪魔が入りません。
物事に集中しやすい時間帯です。

やりたいことができる

早朝に「やりたいこと」をすることもできますし、
仕事を早くから始めることで夕方に「やりたいこと」をすることもできます。
「やりたいこと」をやるための、まとまった時間を作ること出来るのです。

家族と一緒に過ごせる

子どもと一緒に遊んだり絵本を読んだり、家族と一緒に食事をすることができます。
家族と過ごす幸せを感じてください。

朝を味方に


人生に迷っているあなた、朝型生活をはじめて朝を味方につけましょう!

YUITest を使ってJavascriptの単体テストを自動化するまで (後編)

前編 YUITest を使ってJavascriptの単体テストを自動化するまで (前編) - masayuki14’s diary


Git Repository への登録

これまでに使ったファイルをリポジトリに登録しよう。今回はGitをバージョン管理に利用する。実際のプロジェクトでテストを自動化して継続させるのにバージョン管理は必須要素だ。

$ git init
$ git add .
$ git commit -m ‘First commit’

これでGit Repository への登録は完了。

JenkinsのInstall

テストを自動実行するには何らかのツールを使う必要がある。そこで登場するのがJenkinsだ。とにかくインストールして動かしてみよう。大事なのは「射撃しつつ前進」なのだから。

$ brew install jenkins

インストールが終わったらJenkinsを起動してブラウザでアクセスしよう。Jenkinsはデフォルトでポート8080で起動する。

$ java -jar  -Dfile.encoding=utf-8 /usr/local/opt/jenkins/libexec/jenkins.war
# 文字コードをUTF8にするオプションを指定している

ブラウザを開いて http://localhost:8080/ にアクセスするとJenkinsが表示されるはずだ。
task.shを実行するようなジョブを登録すれば、自動的にJenkinsが継続して実行してくれる。
ジョブを登録していこう。

ジョブの登録

メニューから「新規ジョブ作成」を選んで必要な情報を入力しよう。
ジョブ名は好きに登録してOK。今回は「Run YUI Test」とし「フリースタイル・プロジェクトのビルド」を選んで「OK」だ。
次に各種設定を行っていく。

ソースコード管理

「Git」選び Repository URL を入力する。
今回はローカルのファイルシステムリポジトリを用意したので、Repository URL にリポジトリへのパスを入力する。これでGitの設定は完了。

ビルド・トリガ

「SCMをポーリング」にチェックをいれてスケジュールを設定する。定期的にリポジトリを確認し、変更があればビルドをする、という意味だ。スケジュールの書式はcronとほとんど同じ。
15分毎にポーリングさせるため「H/15 * * * * 」と入力する。

ビルド

ビルドの方法を設定する。task.sh の実行がビルドだ。
ビルド手順の追加から「シェルの実行」を選んでシェルスクリプトを記述する。
$ sh task.sh
これで設定は完了。「保存」ボタンで終了しよう。
リポジトリのルートディレクトリでシェルが実行される

これでJenkinsが自動でビルドを実行してくれるようになった。
リポジトリを15分毎に監視して、差分が発生していればビルドを実行してくれる。


テスト結果の収集

これまでの設定だけではJenkinsはビルドの成功・失敗しか結果を教えてくれない。しかしgroverからはテストの結果が出力されている。それをJenkinsに教えてあげれば、Jenkinsで集計をとり表示してくれるようになる。
その方法を見ていこう

テスト結果の出力

テスト結果をJenkinsに取り込ませるにはjUnit形式のXMLファイルに出力しなければならない。
幸いにも grover はこの出力に対応している

$ grover testRunner.html &#8212;junit -o result.junit.xml

これで result.junit.xml にテスト結果が出力される。
これをJenkinsに読み込むように設定する。

ビルドの設定

プロジェクトの設定を開き「ビルド後の処理」に設定を追加する。
ビルド後の処理の追加から「JUnitテスト結果の集計」を選び、テスト結果XMLに「result.junit.xml」を入力して保存。これで設定完了だ。


task.sh の grover コマンドを上記のように編集してgit repositoryにコミットしよう

# task.sh を編集
$ vim task.sh
     #!/bin/sh
     java -jar yuitest-coverage.jar -o target-coveraged.js target.js
     grover --coverage testRunner.html --junit -o result.junit.xml

# 変更内容をコミット
$ git add task.sh
$ git commit -m ‘テスト結果を result.juni.xml に出力’

これでJenkinsのビルド実行を待つ。ビルドに成功すればJenkinsがテスト結果を取り込み表示される。


カバレッジ結果の収集

Pluginのインストール

テスト結果と同様にカバレッジをJenkinsに取り込むにはCobertura Pluginプラグインをインストールする。
「Jenkinsの管理」> 「プラグインの管理」と進み「利用可能」タブを選択。フィルターに「Cobertura Plugin」を入力しすれば簡単に見つけることが出来る。
「インストール」にチェックを入れてインストールしよう。

カバレッジの出力

grover はテスト結果と同様にカバレッジ結果をLcov形式のファイルに出力することが出来る。

$ grover --coverage testRunner.html -co coverage.lcov

これでcoverage.lcov にカバレッジが出力される。

カバレッジ結果の変換

coverage.lcov をそのままJenkinsに取り込めると良いが、残念ながらJenkinsはLCOV形式には対応していない。
Coberturaで対応しているXML形式にcoverage.lcovを変換する必要がある。
変換には https://github.com/eriwen/lcov-to-cobertura-xml で公開されているPythonスクリプトを使う。ここから lcov_cobertura.py をダウンロードしよう。

$ python lcov_cobertura.py coverage.lcov -o coverage.xml

これで変換が完了

ビルドの設定

プロジェクトの設定を開き「ビルド後の処理」に設定を追加する。
ビルド後の処理の追加から「Coberturaカバレッジ・レポートの集計」を選び、Cobertura XMLレポート パターンに「result.junit.xml」を入力して保存する。
これで準備は整った。


task.sh を編集してコミットすれば全ての設定が完了。

$ vim task.sh

     #!/bin/sh
     java -jar yuitest-coverage.jar -o target-coveraged.js target.js
     grover --coverage testRunner.html --junit -o result.junit.xml -co coverage.lcov
     python lcov_cobertura.py coverage.lcov -o coverage.xml

$ git add task.sh lcov_cobertura.py
$ git commit -m ‘Coverageをcoverage.xmlに出力'

CIを回す

あとはJenkinsが自動でテストを実行し結果を集計してくれる。CIを回していこう!
おしまい。