$30 off During Our Annual Pro Sale. View Details »

Practical Metaprogramming in Application

Practical Metaprogramming in Application

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

MOROHASHI Kyosuke

September 20, 2014
Tweet

More Decks by MOROHASHI Kyosuke

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. View Slide

  4. View Slide

  5. https://idobata.io/

    View Slide

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

    View Slide

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

    View Slide

  8. What is
    metaprogramming ?

    View Slide

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

    View Slide

  10. Ruby constructs

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. class Book
    attr_accessor :title
    end

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide


  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

    View Slide

  21. Metaprogramming is
    so familiar in Ruby

    View Slide

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

    View Slide

  23. Practical
    Metaprogramming

    View Slide

  24. Pitfall
    of
    Metaprogramming

    View Slide

  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

    View Slide

  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')
    #=> #@price=1980>

    View Slide

  27. Metaprogramming with
    overuse of reflection API
    tends to be complicated.

    View Slide

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

    View Slide

  29. Extract code from
    'meta' aspect
    &
    write code simply

    View Slide

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

    View Slide

  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(...)

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  41. 1 more example for
    Extraction from
    meta aspect

    View Slide

  42. Post/Comment/Photo
    Censoring system

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 ?

    View Slide

  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)

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  65. Conclusion

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide