Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Meta-programming for Dummies

Meta-programming for Dummies

For a talk I gave on RedDotRuby Conference 2017.

Weiqing

June 22, 2017
Tweet

Other Decks in Technology

Transcript

  1. “the writing of programs that write or manipulate other programs

    (or themselves) as their data” Meta-programming
  2. class A def foo @foo end def foo=(str) @foo =

    str end def bar @bar end def bar=(str) @bar = str end end obj = A.new obj.foo = ‘1’ obj.foo => 1 obj.bar = ‘2’ obj.bar => 2 Why metaprogram?
  3. Why metaprogram? obj = A.new obj.foo = ‘1’ obj.foo =>

    1 obj.bar = ‘2’ obj.bar => 2 class A attr_accessor :foo, :bar end def attr_accessor(*args) args.each do |arg| define_method "#{arg}".to_sym do instance_variable_get("@#{arg}") end define_method "#{arg}=".to_sym do |param| instance_variable_set("@#{arg}", param) end end end
  4. Why metaprogram? class A attr_accessor :foo, :bar end def attr_accessor(*args)

    args.each do |arg| define_method "#{arg}".to_sym do instance_variable_get("@#{arg}") end define_method "#{arg}=".to_sym do |param| instance_variable_set("@#{arg}", param) end end end class A def foo @foo end def foo=(str) @foo = str end def bar @bar end def bar=(str) @bar = str end end Readable Concise
  5. ActiveRecord::Schema.define(version: 20170701000000) do create_table "reports", force: :cascade do |t| t.integer

    "report_id" t.text "content" t.string "title" t.string "workflow_state", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end end class Report < ActiveRecord::Base end report.rb schema.rb Why metaprogram? r = Report.new r.title = ‘Foo’ r.title => Foo r.content = ‘Had a bar.’ r.content => Had a bar. ‘Dynamic’ behaviour
  6. Why metaprogram? • Report model • 3 States - Draft,

    Reviewing, Published • Transitions from one state to another r = Report.new r.state => ‘draft’ r.reviewing? => false r.submit! r.state => ‘reviewing’ r.approve! => ‘published’ r.published? => true Draft Reviewing Published Submit Approve
  7. class Report attr_accessor :state def draft? state == 'draft' end

    def reviewing? state == 'reviewing' end def published? state == 'published' end def submit! return unless draft? state = 'reviewing' end def approve! return unless reviewing? state == 'published' end end Why metaprogram? r = Report.new r.state => ‘draft’ r.reviewing? => false r.submit! r.state => ‘reviewing’ r.approve! => ‘published’ r.published? => true
  8. class Report attr_accessor :state def draft? state == 'draft' end

    def reviewing? state == 'reviewing' end def published? state == 'published' end def submit! return unless draft? state = 'reviewing' end def approve! return unless reviewing? state == 'published' end end Why metaprogram? r = Report.new r.state => ‘draft’ r.reviewing? => false r.submit! r.state => ‘reviewing’ r.approve! => ‘published’ r.published? => true What if … Add pre and post-transition hooks Raise errors for incorrect states Metaprogram! Define states and transitions Populate the necessary methods
  9. class Report include Workflow workflow do state :draft do event

    :submit, transitions_to: :reviewing end state :reviewing do event :approve, transitions_to: :published end state :published end end Why metaprogram? Source: Gem geekq/workflow r = Report.new r.state => ‘draft’ r.reviewing? => false r.submit! r.state => ‘reviewing’ r.approve! => ‘published’ r.published? => true Source Code: https://repl.it/ItY8/0 Readable Concise Reusable Define the logic elsewhere! (~80 lines)
  10. The Ruby Object Model class A def foo ‘a’ end

    end class B < A def foo ‘b’ end def bar ‘bar’ end end Class BasicObject Object Superclass A B obj1 Class Class Module Superclass Superclass Superclass Superclass Class obj2 Class Class obj1 = A.new obj2 = B.new obj1.foo => a obj2.foo => b Singleton Class Singleton Class
  11. Read Core Ruby Docs! Programatically Understanding Code Modifying Behaviour, Code

    or Methods #instance_variables #class_variables #methods #instance_methods #public_methods #private_methods #singleton_methods #class #singleton_class … #respond_to? #respond_to_missing? #method_missing #define_method #class_eval #class_exec #instance_eval #instance_exec #send …
  12. Read Core Ruby Docs! Object Module BasicObject Programatically Understanding Code

    Modifying Behaviour, Code or Methods #instance_variables #class_variables #methods #instance_methods #public_methods #private_methods #singleton_methods #class #singleton_class … #respond_to? #respond_to_missing? #method_missing #define_method #class_eval #class_exec #instance_eval #instance_exec #send …
  13. Gotchas Readability Document & Test Metaprogram Independence
 !meta-(metaprogram)-program More !=

    Good Keep meta-programming code independent! Practise Encapsulation - Try not to break abstraction Don’t meta-program meta-programming code! Meta-programming code should be ‘readable’ More is not always good Maintaining classes with much metaprogramming ain’t easy
  14. Anti-Patterns Readability Document & Test r = some_object POSSIBLE_VERBS =

    ['get', 'put', 'post', 'delete'] POSSIBLE_VERBS.each do |m| define_method(m.to_sym) do |path, *args, &b| r[path].public_send(m.to_sym, *args, &b) end end Short != Readable Implicit vs Explicit r = some_object def get(path, *args &b) r[path].get(*args &b) end def put(path, *args &b) r[path].put(*args &b) end def post(path, *args &b) r[path].post(*args &b) end def delete(path, *args &b) r[path].delete(*args &b) end Source: rest-client/rest-client With Meta-programming Without Meta-programming
  15. Write Documentation Commit with confidence Alternate documentation Anti-Patterns Readability Document

    & Test # attr_accessor(symbol, ...) -> nil # attr_accessor(string, ...) -> nil # # Defines a named attribute for this module, where the name is # <i>symbol.</i><code>id2name</code>, creating an instance variable # (<code>@name</code>) and a corresponding access method to read it. # Also creates a method called <code>name=</code> to set the attribute. # String arguments are converted to symbols. # # module Mod # attr_accessor(:one, :two) # end # Mod.instance_methods.sort #=> [:one, :one=, :two, :two=] def attr_accessor(*several_variants) #This is a stub, used for indexing end Source: Workflow
  16. Anti-Patterns Readability Document & Test class Report < ActiveRecord::Base include

    Workflow workflow do state :draft do event :submit, :transitions_to => :reviewing end state :reviewing do event :publish, :transitions_to => :published end state :published end end test 'question methods for state' do r = Report.first r = assert_state r, 'draft' assert r.draft? assert !r.reviewing? end ... Source: Workflow Write Documentation Commit with confidence Alternate documentation
  17. Source: Gem geekq/workflow Source Code: https://repl.it/ItY8/0 module workflow class Spec

    attr_accessor :states, :events def initialize(&spec) @states, @events = [], {} instance_eval(&spec) end def state(name, &state_spec) @curr_state = name @states << name instance_eval(&state_spec) if state_spec end def event(name, transitions_to: nil) @events[name] = [@curr_state, transitions_to] end end module InstanceMethods def initialize(*args) @workflow_state = self.class.spec&.states[0] super(*args) end def workflow_state @workflow_state end end … module ClassMethods attr_accessor :spec def workflow(&spec) @spec = Spec.new(&spec) populate_methods @spec end def populate_methods(spec) spec&.events&.each do |event, states| module_eval do define_method "#{event}!".to_sym do raise Error if workflow_state.to_sym != states[0].to_sym @workflow_state = states[1].to_sym end end end spec&.states&.each do |curr_state| module_eval do define_method "#{curr_state}?".to_sym do workflow_state.to_sym == curr_state.to_sym end end end end end def self.included(klass) klass.send :include, InstanceMethods klass.extend ClassMethods end end Sample Implementation for Workflow