Slide 1

Slide 1 text

Practical Metaprogramming in Application Kyosuke MOROHASHI (@moro) @2014-09-20 RubyKaigi 2014

Slide 2

Slide 2 text

ॾڮګհ(@moro) Kyosuke MOROHASHI

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

https://idobata.io/

Slide 6

Slide 6 text

Practical Metaprogramming in Application MOROHASHI Kyosuke (@moro) @2014-09-20 RubyKaigi 2014

Slide 7

Slide 7 text

GOAL of the talk Feel close to Ruby's metaprogramming. Understand practical use of metaprogramming.

Slide 8

Slide 8 text

What is metaprogramming ?

Slide 9

Slide 9 text

“Metaprogramming is writing code that manipulates language constructs at runtime Metaprogramming Ruby 2

Slide 10

Slide 10 text

Ruby constructs

Slide 11

Slide 11 text

class / module object (& its state, @ivar) method a series of procedures

Slide 12

Slide 12 text

class Book def title @title end def title=(title) @title = title end end

Slide 13

Slide 13 text

class Book def title @title end def title=(title) @title = title end end

Slide 14

Slide 14 text

You will think... Is there more efficient way using repeated 'title' by manipulating language constructs ?

Slide 15

Slide 15 text

class Book attr_accessor :title end

Slide 16

Slide 16 text

Just example, real code is written in C. 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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Define methods dynamically. define_method(:title) do instance_variable_get("@title") end define_method("title=") do |val| instance_variable_set("@title", val) end

Slide 19

Slide 19 text

Manipulate @ivar based on its name define_method(:title) do instance_variable_get("@title") end define_method("title=") do |val| instance_variable_set("@title", val) end

Slide 20

Slide 20 text

“ the ability to examine and modify the structure and behavior of the program at runtime Extracted from http://en.wikipedia.org/wiki/Reflection_(computer_programming) Reflection APIs

Slide 21

Slide 21 text

Metaprogramming is so familiar in Ruby

Slide 22

Slide 22 text

Sum m ary What is metaprogramming ? Writing code that manipulates language constructs So familiar in Ruby Rubyists are using them everyday

Slide 23

Slide 23 text

Practical Metaprogramming

Slide 24

Slide 24 text

Pitfall of Metaprogramming

Slide 25

Slide 25 text

DON'T READ: Overuse of metaprogramming Book = Class.new do define_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

Slide 26

Slide 26 text

Standard Ruby code 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') #=> #

Slide 27

Slide 27 text

Metaprogramming with overuse of reflection API tends to be complicated.

Slide 28

Slide 28 text

You will think... Is there any way to write simple code with metaprogramming?

Slide 29

Slide 29 text

Extract code from 'meta' aspect & write code simply

Slide 30

Slide 30 text

ActiveRecord::Base.has_many assoc_name Specifies a one-to-many association named assoc_name.

Slide 31

Slide 31 text

Many methods are added class Books < ActiveRecord::Base has_many :reviews end Book#reviews #reviews= #review_ids #review_ids= ... Book#reviews.build reviews.create reviews.each {|review| ... } reviews.where(...)

Slide 32

Slide 32 text

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 en Seems difficult internal (not actual code)

Slide 33

Slide 33 text

ActiveRecord::Base.has_many assoc_name Define wrapper methods to access `association object` that represents a one-to-many association.

Slide 34

Slide 34 text

Book#reviews Returns an association object that represents a book has_many reviews. NOT returns reviews directly

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Has owner and target Define join relations on owner.pk = target.fk Load target & cache them Manage create/delete and callbacks. Association Object

Slide 37

Slide 37 text

= Active Record Associations This is the root class of all associations ('+ Foo' signifies an included module Foo): Association SingularAssociation HasOneAssociation HasOneThroughAssociation + ThroughAssociation BelongsToAssociation BelongsToPolymorphicAssociation CollectionAssociation HasAndBelongsToManyAssociation HasManyAssociation HasManyThroughAssociation + ThroughAssociation

Slide 38

Slide 38 text

Returns "association object" instead of children directly. Then, what is useful API to access association?

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Sum m ary Points for Practical metaprogramming Extract code from 'meta' aspect & write code simply Define thin wrapper with reflection APIs.

Slide 41

Slide 41 text

1 more example for Extraction from meta aspect

Slide 42

Slide 42 text

Post/Comment/Photo Censoring system

Slide 43

Slide 43 text

Censors post, comment and photo. Submit each content to external censoring service API. Post: title + body Comment: body Photo: public URL

Slide 44

Slide 44 text

class Post < AR::Base after_save :submit_to_censoring_service private def submit_to_censoring_service content = [title, body].join("\n\n") url = Rails.config.censoring_service_url req = Net::HTTP::Post.new(url.path) req.set_form_data(id: to_global_id, text: conte Net::HTTP.start(url.hostname, url.port) do |htt http.request(req) end end

Slide 45

Slide 45 text

class Comment < AR::Base ... def submit_to_censoring_service content = body ... end end class Photo < AR::Base ... def submit_to_censoring_service content = "http://img.example.com/#{id}" ... end end

Slide 46

Slide 46 text

You will think... First of all, extract obvious duplications.

Slide 47

Slide 47 text

class Post < AR::Base after_save :submit_to_censoring_service private def submit_to_censoring_service content = [title, body].join("\n\n") url = Rails.config.censoring_endpoint req = Net::HTTP::Post.new(url.path) req.set_form_data(id: to_global_id, text: conte Net::HTTP.start(url.hostname, url.port) do |htt http.request(req) end end

Slide 48

Slide 48 text

class Post < AR::Base after_save :submit_to_censoring_service private def submit_to_censoring_service CensorRequest.new( id: to_global_id, text: [title, body].join("\n\n") ).submit end end

Slide 49

Slide 49 text

class CensorRequest @@url = URI(Rails.config.censoring_endpoint) def initialize(content) @content = content end def submit req = Net::HTTP::Post.new(@@url.path) req.set_form_data(content) Net::HTTP.start(@@url.hostname, @@url.port) do http.request(req) end end end

Slide 50

Slide 50 text

class Post < AR::Base after_save :submit_to_censoring_service private def submit_to_censoring_service CensorRequest.new( id: to_global_id, text: [title, body].join("\n\n") ).submit end end

Slide 51

Slide 51 text

You will think... Extract code from 'meta' aspect There should be a class which builds content per each censored class & submits it to the service ?

Slide 52

Slide 52 text

class ContentCensor def initialize(type, &builder) @type = type @builder = builder end def submit(record) CensorRequest.new( id: record.global_id, @type => @builder.call(record) ).submit end end # ---------------- censor = ContentCensor.new(:text) do |post| [post.title, post.body].join("\n\n") end censor.submit(post)

Slide 53

Slide 53 text

class Post < AR::Base @@censor = ContentCensor.new(:text) do |post| [post.title, post.body].join("\n\n") end after_save :submit_to_censoring_service private def submit_to_censoring_service @@censor.submit(self) end end

Slide 54

Slide 54 text

class Comment < AR::Base @@censor = ContentCensor.new(:text) do |comment| comment.body end after_save :submit_to_censoring_service private def submit_to_censoring_service @@censor.submit(self) end end

Slide 55

Slide 55 text

class Photo < AR::Base @@censor = ContentCensor.new(:image) do |photo| "http://img.example.com/#{photo.id}" end after_save :submit_to_censoring_service private def submit_to_censoring_service @@censor.submit(self) end end

Slide 56

Slide 56 text

You will think... Define thin wrapper with reflection APIs. Does extraction of ContentCensor call make the code cleaner ?

Slide 57

Slide 57 text

module CensorDsl def censor_content(type, &block) censor = CensorContent.new(type, block) after_save {|record| censor.submit(record) } end end

Slide 58

Slide 58 text

class Post < AR::Base extend CensorDsl censor_content(:text) do |post| [post.title, post.body].join("\n\n") end end

Slide 59

Slide 59 text

You will think... protip: Try to clean up codes like Rails framework with ActiveSupport::Concern

Slide 60

Slide 60 text

module Censorable extend ActiveSupport::Concern module ClassMethods def censor_content(type, &block) censor = Censorable::Censor.new(type, block) after_save {|record| censor.submit(record) } end end class Censor ... class Request ... end

Slide 61

Slide 61 text

class Post < ActiveRecord::Base include Censorable censor_content(:text) do |post| [post.title, post.body].join("\n\n") end end

Slide 62

Slide 62 text

Before class Post < ActiveRecord::Base after_save :submit_to_censoring_service private def submit_to_censoring_service content = [title, body].join("\n\n") url = Rails.config.censoring_service_url req = Net::HTTP::Post.new(url.path) req.set_form_data(id: to_global_id, text: conte Net::HTTP.start(url.hostname, url.port) do |htt http.request(req) end end

Slide 63

Slide 63 text

After class Post < ActiveRecord::Base include Censorable censor_content(:text) do |post| [post.title, post.body].join("\n\n") end end

Slide 64

Slide 64 text

Sum m ary Points for Practical metaprogramming Extract code from 'meta' aspect & write code simply Define thin wrapper with reflection APIs. REVISED

Slide 65

Slide 65 text

Conclusion

Slide 66

Slide 66 text

GOAL of the talk Feel closer to Ruby's metaprogramming. Understand practical use of metaprogramming.

Slide 67

Slide 67 text

There isn't so much difference between standard programming and metaprogramming while we code in Ruby and/or Rails.

Slide 68

Slide 68 text

It's helpful to extract code from 'meta' aspect & code simply to avoid complicated code by over use of metaprogramming.

Slide 69

Slide 69 text

But, I know it's so fun to use reflection APIs that I recommend to practice extreme metaprogramming :-)

Slide 70

Slide 70 text

You should extract code from 'meta' aspects step by step. Ruby's flexibility helps you.

Slide 71

Slide 71 text

“Ruby͸܅Λ৴པ͢ΔɻRuby͸܅Λ ෼ผͷ͋ΔϓϩάϥϚͱͯ͠ѻ͏ɻ Ruby͸ϝλϓϩάϥϛϯάͷΑ͏ͳ ڧྗͳྗΛ༩͑Δɻͨͩ͠ɺେ͍ͳ Δྗʹ͸ɺେ͍ͳΔ੹೚͕൐͏͜ͱ Λ๨Εͯ͸͍͚ͳ͍ɻͦΕͰ͸ɺ RubyͰͨͷ͍͠ϓϩάϥϛϯάΛɻ ʮϝλϓϩάϥϛϯάRubyʯ ংจΑΓ Matz says: