Practical Meta Programmingon Rails Application

Railsアプリでの実用的メタプログラミング@2013-12-17 Rubyの技術を語る1日 in 品川

MOROHASHI Kyosuke
13年12月16日月曜日

諸橋恭介 (@moro)

Kyosuke MOROHASHI
13年12月16日月曜日

13年12月16日月曜日

13年12月16日月曜日

Agenda✓メタプログラミングとはなにか✓

リフレクションtoolbox✓

実用メタプログラミング✓

共通機能の抽出✓ライブラリの作り方

13年12月16日月曜日

今日のゴール
13年12月16日月曜日

✓

Rubyではメタプログラミングは"ふつう"だと思えるようになる✓

メタプログラミング用のRubyの機能について、概略をつかむ✓

実践的にメタプログラミングするときの注意点・考え方を理解する

13年12月16日月曜日

http://www.amazon.co.jp/exec/obidos/ASIN/4048687158/morodiary05-22/ref=noism
13年12月16日月曜日

✓Rubyではメタプログラミングは"ふつう"だと思えるようになる✓

メタプログラミング用のRubyの機能について、概略をつかむ✓

実践的にメタプログラミングするときの注意点・考え方を理解する

13年12月16日月曜日

メタプログラミングとはなにか
13年12月16日月曜日

“

メタプログラミングとは、言語要素を実行時に操作するコードを記述すること「メタプログラミングRuby」

13年12月16日月曜日

Rubyの言語要素

13年12月16日月曜日

✓

クラス

✓

モジュール

✓

オブジェクト(とその状態:インスタンス変数)

✓メソッド

✓

手続き

13年12月16日月曜日

class Book
def title@titleend
def title=(title)@title = title
end
end
13年12月16日月曜日

class Book
def title@titleend
def title=(title)@title = title
end
end
13年12月16日月曜日

言語要素を操作して何度も出ている'title'からいろいろできないか?
13年12月16日月曜日

class Book
attr_accessor :title
end

13年12月16日月曜日

def attr_accessor(name)define_method(name) do
instance_variable_get("@#{name}")end

define_method("#{name}=") do |val|instance_variable_set("@#{name}", val)
end
end

13年12月16日月曜日

define_method(:title) do
instance_variable_get("@title")enddefine_method("title=") do |val|instance_variable_set("@title", val)
end

13年12月16日月曜日

define_method(:title) do
instance_variable_get("@title")enddefine_method("title=") do |val|instance_variable_set("@title", val)end

メソッドを動的に追加
13年12月16日月曜日

define_method(:title) do
instance_variable_get("@title")enddefine_method("title=") do |val|instance_variable_set("@title", val)
end

インスタンス変数を変数名を使って操作
13年12月16日月曜日

Rubyでは、メタプ
ログラミングとそうでないものとに明確な区別はない
13年12月16日月曜日

まとめ: メタプログラミングとは✓言語要素を操作する✓

実は特別なものでない✓

Rubyでは日常的にやっている

13年12月16日月曜日

リフレクションtoolbox
13年12月16日月曜日

“リフレクション (reflection) とは、プログラムの実行過程でプログラム自身の構造を読み取ったり書き換えたりする技術のことである。
http://ja.wikipedia.org/wiki/リフレクション _(情報工学)

13年12月16日月曜日

http://www.amazon.co.jp/exec/obidos/ASIN/4048687158/morodiary05-22/ref=noism
13年12月16日月曜日

Class.is_a?(Object) #=> true✓

Rubyではクラスそれ自身もオブジェク
トである✓

Moduleクラスを継承した、Classクラスのオブジェクト✓

変数に入れたり、引数にしたりできる

13年12月16日月曜日

klasses = {'array klass' => Array,'hash klass' => Hash,}def class_name(klass)
klass.nameendklasses['array klass'].new #=> []class_name(klasses['hash klass']) #=> "Hash"

13年12月16日月曜日

Class.is_a?(Object) #=> true✓

クラス定義を変更するメソッドがある✓

includeやextendやdefine_method
もModuleクラスのメソッド

13年12月16日月曜日

ブロック(Proc)✓

処理そのものをオブジェクトとして扱える✓

Rubyの大きな魅力の一つ

13年12月16日月曜日

ブロック(Proc)# イテレータや処理の差し替え
multiplier = ->(i) { i * 2 }square= ->(i) { i ** 2 }[1, 2, 3].map(&multiplier)[1, 2, 3].map(&square)# リソースの開閉
File.open(path, 'w') {|f| f.puts('content') }# 遅延評価scope :fresh,-> { where('created_at > ?', 3.hour.ago) }13年12月16日月曜日

2010年12月4日(土) 札幌Ruby会議03

Rubyの教えてくれたこと— You must unlearn what you have learned.

島田 浩二
koji.shimada@enishi-tech.com
http://www.slideshare.net/snoozer05/20101204-youmustunlearnwhatyouhavelearned
2010年12月5日日曜日
13年12月16日月曜日

Object#send✓

引数で指定したメソッドを呼び出す✓

つまり、呼び出すメソッドをプログラム的に変更できる

13年12月16日月曜日

Object#send
def up_or_down(condition)
up_or_down = condition ? :upcase ? :downcase'Ruby'.send(up_or_down)
end
up_or_down(true) # => 'RUBY'
up_or_down(false) # => 'ruby'

13年12月16日月曜日

define_method✓

モジュールやクラスに、インスタンスメソッドを追加するメソッド✓✓

defのリフレクション版

メソッドの中身をブロックで書く✓

13年12月16日月曜日

そのためブロック外の変数が見えるdefine_methodclass Foo
foo = 'FOO'
define_method(:foo1) doputs foo + '1'
end
def foo2
puts foo + '2'
end
end
obj = Foo.new
obj.foo1 #=> 'FOO1'
obj.foo2 #=> NameError
13年12月16日月曜日

class_eval/instance_eval✓

文字列で書かれたRuby式やブロックを、クラスやインスタンスのコンテキストで評価する

13年12月16日月曜日

class_eval/instance_eval✓✓

class_eval (module_eval)を使うシーン✓

Classクラスのprivateメソッドを呼んだり、✓Classにあとからメソッドを追加したりする

instance_eval を使うシーン✓

インスタンスのインスタンス変数を参照する✓

privateメソッドを呼ぶ

13年12月16日月曜日

class Foo
def initialize(message)@message = message
end
end
Foo.class_eval %q[
def greet"#{@message} you!"end]
foo = Foo.new('hi')p foo.greet#=> 'hi you!'p foo.instance_eval { @message } #=> 'hi'
13年12月16日月曜日

instance_variable_set/
instance_variable_get✓

インスタンス変数を名前ベースで取得したり設定したりできる✓

おなじようにclass_varaible_set/getもある

13年12月16日月曜日

class Foo
def initialize(message)@message = message
end
end
foo = Foo.new('hi')
foo.instance_variable_get('@message') #=> 'hi'

13年12月16日月曜日

method_missing
✓!
!
意
注
い
取り扱

存在しないメソッドを呼ばれた
時に呼ばれるメソッド✓

ActiveRecordの属性取得メソッドなどに使われている

13年12月16日月曜日

method_missing!
!
意
注
い取り扱

class Foodef method_missing(method, *args, &block)
if method = :hi'hello'
else
super
end
end
end
Foo.new.hi #=> 'hello'
13年12月16日月曜日

include,extend✓

mixinもクラスやモジュールの言語要素を操作している✓

includeはそのクラスのインスタンスメソッドを追加する✓

extendはそのオブジェクトに(特異)メソッドを追加する

13年12月16日月曜日

included, extended✓

モジュールがincludeされたりextendされたりすると実行されるフック✓

継承のタイミングで実行されるinherited
メソッドもある13年12月16日月曜日

included, extended
module MyModule
def self.included(base)"included by #{base}"end
def self.extended(obj)"extended by #{obj}"
endendclass MyClass
include MyModule
end# => "included by MyClass"

MyClass.new.extend(MyModule) # => "extended by #<MyClass:0x007fd
13年12月16日月曜日

ActiveSupport::Concern✓

Railsによるincludeの拡張✓

そのモジュールに ClassMethods というモジュールがあれば、それをextendする✓

included {} という、includedフック相当のクラスマクロがある

13年12月16日月曜日

ActiveSupport::Concern
module MyModule
included(base)"included by #{base}"end
module ClassMethods
def hi; 'hello' ; end
end
endclass MyClass
include MyModule # => "included by MyClass"end
MyClass.hi
13年12月16日月曜日# => "hello"まとめ: リフレクションtoolbox✓

Rubyのクラスは「やわらかい」✓✓

操作するためのメソッドがたくさんある「メタプログラミングRuby」おすすめ

13年12月16日月曜日

実用メタプロ
グラミング
13年12月16日月曜日

メタプログラ
ミングの落とし穴
13年12月16日月曜日

Book = Class.new dodefine_method(:initialize) do |attrs|
attrs.each do |key, value|
if respond_to?("#{key}=")send("#{key}=", value)else
instance_variable_set("@#{key}", value)
end
end
end
define_method(:price=) do |price_str|
instance_variable_set('@price',price_str.delete(',').to_i)
end
end
p Book.new(author: 'moro', price: '1,980')
13年12月16日月曜日

class Book
def initialize(attrs)@author= attrs[:author]
self.price = attrs[:price]end
def price=(price_str)@price = Integer(price_str.delete(','))
end
end
Book.new(author: 'moro', price: '1,980')#=> #<Book:0x007faddb148b48 @author="moro",@price=1980>
13年12月16日月曜日

✓

メタプログラミングのコードは"難しい"✓とりわけリフレクション✓

send期, eval期

13年12月16日月曜日

メタに考えてベタに作る
13年12月16日月曜日

ActiveRecord::Base.has_many assoc_nameassoc_name で指定された、複数の子レコードへの関連を定義する。

13年12月16日月曜日

class Books < ActiveRecord::Basehas_many :reviews
end
Book#reviews#reviews=#review_ids#review_ids=...
Book#reviews.build
reviews.create
reviews.each {|review| ... }reviews.where(...)

たくさんメソッドができる
13年12月16日月曜日

def has_many(name, opts = {})class_eval <<-RUBY
def #{name}klass = #{name.to_s.classify}
klass.where(#{fk}: id)
end
def #{name}=(value)
values.each do |value|#{name.to_s.classify}.create!(...)
end
end
RUBY
end

難しそうな実装イメージ

13年12月16日月曜日

ActiveRecord::Base.has_many assoc_nameassoc_name で指定された、複数の子レコードへの関連を扱うオブジェクトのためのラッパーメソッドを定義する。

13年12月16日月曜日

Book#reviews✓

紐付いているレビューを返す、のではない✓

親に紐づくレビューがあるという関連を表すオブジェクトを返す

13年12月16日月曜日

def define_readers
mixin.class_eval <<-CODE, __FILE__, __LINE__ +def #{name}(*args)
association(:#{name}).reader(*args)
end
CODE
end

関連を表すオブジェクトを返す
13年12月16日月曜日

Associationの抽出✓

owner と target がいる✓

owner の id とtarget の fk で検索する scope を作る✓

target をロードし、適切にメモ化✓

追加削除のコールバックを管理する

13年12月16日月曜日

= Active Record Associations
This is the root class of all associations('+ Foo' signifies an included module Foo):
AssociationSingularAssociation
HasOneAssociation
HasOneThroughAssociation+ ThroughAssociation
BelongsToAssociation
BelongsToPolymorphicAssociation
CollectionAssociation
HasAndBelongsToManyAssociation
HasManyAssociation
HasManyThroughAssociation+ ThroughAssociation
13年12月16日月曜日

✓親レコードから子レコードを引くメソッドを作る✓

親レコードと子レコードとの関連を表すクラスを作る

13年12月16日月曜日

def define_readers
mixin.class_eval <<-CODE, __FILE__, __LINE__ +def #{name}(*args)
association(:#{name}).reader(*args)
end
CODE
end

リフレクションで薄いラッパーを作る
13年12月16日月曜日

まとめ:
実用メタプログラミング✓

メタに考えてベタに作る✓

リフレクションでつなぐ13年12月16日月曜日

共通機能の抽出
13年12月16日月曜日

投稿チェック
機能の例
13年12月16日月曜日

✓

記事(Post)とコメント(Comment)と画像(Photo)の投稿内容をチェック✓

それぞれごとの内容で外部サービスに投稿したい✓Postはタイトルと本文、コメントは本文、画像は表示URL

13年12月16日月曜日

class Post < AR::Baseafter_save :submit_content_monitoring
privatedef submit_content_monitoring
content = [title, body].join("\n\n")url = Rails.config.censoring_endpoint
req = Net::HTTP::Post.new(url.path)
req.set_form_data('content' => content)
Net::HTTP.start(url.hostname, url.port) do |htt
http.request(req)endend
13年12月16日月曜日

class Comment < AR::Base...
def submit_content_monitoring
content = body
...
end
end
class Photo < AR::Base...
def submit_content_monitoring
content ="<img src='http://img.example.com/#{id}' />"
...
end
end
13年12月16日月曜日

共通部分を探す
13年12月16日月曜日

class Post < AR::Baseafter_save :submit_content_monitoring
privatedef submit_content_monitoring
content = [title, body].join("\n\n")url = Rails.config.censoring_endpoint
req = Net::HTTP::Post.new(url.path)
req.set_form_data('content' => content)
Net::HTTP.start(url.hostname, url.port) do |htt
http.request(req)endend
13年12月16日月曜日

class Post < AR::Baseafter_save :submit_content_monitoring
privatedef submit_content_monitoring
Censoring.new([title, body].join("\n\n"),Rails.config.censoring_endpoint).submit
end
end

13年12月16日月曜日

class Censoring
def initialize(content, url)
@content = content
@url
= url
end

def submit
req = Net::HTTP::Post.new(@url.path)
req.set_form_data('content' => @content)
Net::HTTP.start(@url.hostname, @url.port) do |hhttp.request(req)
end
end
end
13年12月16日月曜日

class Post < AR::Baseafter_save :submit_content_monitoring
privatedef submit_content_monitoring
Censoring.new([title, body].join("\n\n"),Rails.config.censoring_endpoint).submit
end
end

13年12月16日月曜日

メタに考えてベタに作る"対象クラスごとに、投稿チェック内容を組み立て送信する"クラスがほしい
13年12月16日月曜日

class CensorAdapter
def initialize(endpoint, &block)@endpoint= endpoint@content_builder = blockend
def submit(record)
content = @content_builder.call(record)
Censoring.new(content, @endpoint).submit
end
end# ---------------post_adapter = CensorAdapter.new(endpoint) do |post|[post.title, post.body].join("\n\n")end
post_adapter.submit(post)
13年12月16日月曜日

class Post < AR::Base@@censor_adapter =CensorAdapter.new(config.endpoint) do |post|[post.title, post.body].join("\n\n")end
after_save :submit_content_monitoring
privatedef submit_content_monitoring@@censor_adapter.submit(self)
end
end

13年12月16日月曜日

class Comment < AR::Base@@censor_adapter =CensorAdapter.new(config.endpoint) do |comment|comment.bodyendafter_save :submit_content_monitoring
privatedef submit_content_monitoring@@censor_adapter.submit(self)
end
end

13年12月16日月曜日

class Photo < AR::Base@@censor_adapter =CensorAdapter.new(config.endpoint) do |photo|"<img src='http://img.example.com/#{photo.id}' />"end
after_save :submit_content_monitoring
privatedef submit_content_monitoring@@censor_adapter.submit(self)
end
end

13年12月16日月曜日

リフレクションでつなげる

投稿するためのメソッドを追加してまわるのを、なんとかできないか
13年12月16日月曜日

module CeonsoringDsl
def censor_content(url, &block)adapter = CensorAdapter.new(url, block)
after_save {|record| adapter.submit(record) }end
end

13年12月16日月曜日

class Post < AR::Baseextend CensoringDsl
censor_content(config.endpoint) do |post|[post.title, post.body].join("\n\n")end
end

13年12月16日月曜日

protip:

作ったいろんなクラスをConcernにまとめる

13年12月16日月曜日

module Censorable
extend ActiveSupport::Concern

module ClassMethods
def censor_content(url, &block)adapter = Censorable::Adapter.new(url, block
after_save {|record| adapter.submit(record)end
endclass Adapter...
class Request...
end
13年12月16日月曜日

class Post < ActiveRecord::Baseinclude Censorable

censor_content(config.censoring_endpoint) do |pos
[post.title, post.body].join("\n\n")endend

13年12月16日月曜日

小さく分けてテストする

ベタに作られた共通部分はテストしやすい

13年12月16日月曜日

✓

Censorable::Request✓

Censorable::Adapter✓

拡張されるAR(AMo)モデル

13年12月16日月曜日

Censorable::Request#initialize(content, url)
送信内容とエンドポイントを受け取り#submit
そこにPOSTする#submit の通信部分をモックすると、テストしやすい(FakeWeb, WebMockなど)
13年12月16日月曜日

Censorable::Adapter#initialize(url, &block)
エンドポイントとデータ変換方法(Proc)を受け取り#submit(record)
初期化時のurlと、recordから作った送信内容でRequestを作り、submit()&blockとrecordを変えながら試すことで変換エラーなどのバリエーションテストOK
13年12月16日月曜日

AR(AMo)モデル#create!
保存されるデータから適切に作られた内容が監視サービスに送信される

拡張されたモデルの動きは「外側から」の振る舞いをテストする。
(ここでも、必要に応じて通信部分をモック)

13年12月16日月曜日

まとめ:
共通機能の抽出✓

メタに考えてベタに作る✓

リフレクションでつなぐ✓

小さく分けてテストする

13年12月16日月曜日

まとめ
13年12月16日月曜日

今日のゴール
13年12月16日月曜日

✓

Rubyではメタプログラミングは"ふつう"だと思えるようになる✓

メタプログラミング用のRubyの機能について、概略をつかむ✓

実践的にメタプログラミングするときの注意点・考え方を理解する

13年12月16日月曜日

RubyやRailsの開発では、メタプログラミングと、そうでないものの区別は曖昧。

13年12月16日月曜日

いつも使っている機能が、実は動的メソッド定義だったりするし、それを自分で作ることも簡単

13年12月16日月曜日

そこでやり過ぎないために、「メタに考えベタに書く」ことを心がける(いっぽうでリフレクションを使いまくる"練習"もしてみるとたのしいです)

13年12月16日月曜日

包括的な完成品を目指すのではなく、実アプリの機能を、少しずつ注意深く"メタ"にしていくのが大事

13年12月16日月曜日

それができる柔軟性がRubyの良さ

13年12月16日月曜日

Matz says:“

Rubyは君を信頼する。Rubyは君を分別のあるプログラマとして扱う。
Rubyはメタプログラミングのような強力な力を与える。ただし、大いなる力には、大いなる責任が伴うことを忘れてはいけない。それでは、Rubyでたのしいプログラミングを。
「メタプログラミングRuby」 序文より13年12月16日月曜日