Practical Metaprogramming in Application

Practical Metaprogramming in Application

RubyKaigi 2014 talk. Points for *practical* use of metaprogramming in Ruby.

70e13d9877054026fda46d5a5b53a236?s=128

MOROHASHI Kyosuke

September 20, 2014
Tweet

Transcript

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

  2. ॾڮګհ(@moro) Kyosuke MOROHASHI

  3. None
  4. None
  5. https://idobata.io/

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

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

    practical use of metaprogramming.
  8. What is metaprogramming ?

  9. “Metaprogramming is writing code that manipulates language constructs at runtime

    Metaprogramming Ruby 2
  10. Ruby constructs

  11. class / module object (& its state, @ivar) method a

    series of procedures
  12. class Book def title @title end def title=(title) @title =

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

    title end end
  14. You will think... Is there more efficient way using repeated

    'title' by manipulating language constructs ?
  15. class Book attr_accessor :title end

  16. 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
  17. define_method(:title) do instance_variable_get("@title") end define_method("title=") do |val| instance_variable_set("@title", val) end

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

    instance_variable_set("@title", val) end
  19. 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
  20. “ 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
  21. Metaprogramming is so familiar in Ruby

  22. Sum m ary What is metaprogramming ? Writing code that

    manipulates language constructs So familiar in Ruby Rubyists are using them everyday
  23. Practical Metaprogramming

  24. Pitfall of Metaprogramming

  25. 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
  26. 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') #=> #<Book:0x007faddb148b48 @author="moro", @price=1980>
  27. Metaprogramming with overuse of reflection API tends to be complicated.

  28. You will think... Is there any way to write simple

    code with metaprogramming?
  29. Extract code from 'meta' aspect & write code simply

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

  31. 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(...)
  32. 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)
  33. ActiveRecord::Base.has_many assoc_name Define wrapper methods to access `association object` that

    represents a one-to-many association.
  34. Book#reviews Returns an association object that represents a book has_many

    reviews. NOT returns reviews directly
  35. def define_readers mixin.class_eval <<-CODE, __FILE__, __LINE__ + def #{name}(*args) association(:#{name}).reader(*args)

    end CODE end Returns association object.
  36. Has owner and target Define join relations on owner.pk =

    target.fk Load target & cache them Manage create/delete and callbacks. Association Object
  37. = 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
  38. Returns "association object" instead of children directly. Then, what is

    useful API to access association?
  39. 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
  40. Sum m ary Points for Practical metaprogramming Extract code from

    'meta' aspect & write code simply Define thin wrapper with reflection APIs.
  41. 1 more example for Extraction from meta aspect

  42. Post/Comment/Photo Censoring system

  43. Censors post, comment and photo. Submit each content to external

    censoring service API. Post: title + body Comment: body Photo: public URL
  44. 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
  45. 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
  46. You will think... First of all, extract obvious duplications.

  47. 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
  48. 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
  49. 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
  50. 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
  51. 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 ?
  52. 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)
  53. 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
  54. 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
  55. 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
  56. You will think... Define thin wrapper with reflection APIs. Does

    extraction of ContentCensor call make the code cleaner ?
  57. module CensorDsl def censor_content(type, &block) censor = CensorContent.new(type, block) after_save

    {|record| censor.submit(record) } end end
  58. class Post < AR::Base extend CensorDsl censor_content(:text) do |post| [post.title,

    post.body].join("\n\n") end end
  59. You will think... protip: Try to clean up codes like

    Rails framework with ActiveSupport::Concern
  60. 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
  61. class Post < ActiveRecord::Base include Censorable censor_content(:text) do |post| [post.title,

    post.body].join("\n\n") end end
  62. 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
  63. After class Post < ActiveRecord::Base include Censorable censor_content(:text) do |post|

    [post.title, post.body].join("\n\n") end end
  64. Sum m ary Points for Practical metaprogramming Extract code from

    'meta' aspect & write code simply Define thin wrapper with reflection APIs. REVISED
  65. Conclusion

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

    practical use of metaprogramming.
  67. There isn't so much difference between standard programming and metaprogramming

    while we code in Ruby and/or Rails.
  68. It's helpful to extract code from 'meta' aspect & code

    simply to avoid complicated code by over use of metaprogramming.
  69. But, I know it's so fun to use reflection APIs

    that I recommend to practice extreme metaprogramming :-)
  70. You should extract code from 'meta' aspects step by step.

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

    says: