Slide 1

Slide 1 text

Metaprogramming Weiqing Software Engineer Ruby Edition

Slide 2

Slide 2 text

Why this talk?

Slide 3

Slide 3 text

3 years ago …

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

Why Meta-programming Tips to Meta-Programming Sustainable Meta-Programming

Slide 7

Slide 7 text

Meta-programming

Slide 8

Slide 8 text

‘code that writes more code’

Slide 9

Slide 9 text

“the writing of programs that write or manipulate other programs (or themselves) as their data” Meta-programming

Slide 10

Slide 10 text

Why metaprogram?

Slide 11

Slide 11 text

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?

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Why metaprogram? Draft Reviewing Published Submit Approve Draft Preview Review Save Submit Finalise Published Publish Confirm

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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)

Slide 20

Slide 20 text

Why metaprogram? Domain specific language(s), and/or implicit behaviour Dynamic Behaviour Readable Concise DRY

Slide 21

Slide 21 text

Why metaprogram?

Slide 22

Slide 22 text

3 tips to start metaprogramming …

Slide 23

Slide 23 text

1. Learn the Ruby Object Model Get the correct mental model

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

2. Learn how to ‘modify’ behaviour Understand how Ruby is evaluated

Slide 26

Slide 26 text

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 …

Slide 27

Slide 27 text

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 …

Slide 28

Slide 28 text

3. Figure out the ideal DSL or behaviour and implement Iterate, copy, iterate

Slide 29

Slide 29 text

Read Copy Get inspiration from other people’s code!

Slide 30

Slide 30 text

My Thoughts Doing meta-programming sustainably

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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 # symbol.id2name, creating an instance variable # (@name) and a corresponding access method to read it. # Also creates a method called name= 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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

My Thoughts Doing meta-programming sustainably Dynamic Behaviour Readable Concise DRY Implicit Behaviour Hard to Debug Takes time to design

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

Questions, Views and Comments? weiqingtoh

Slide 40

Slide 40 text

The End!

Slide 41

Slide 41 text

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