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

Metaprogramming in Ruby

Metaprogramming in Ruby

Metaprogramming in Ruby for beginner

Avatar for Tom Ta

Tom Ta

June 08, 2016

Other Decks in Programming

Transcript

  1. :whoami { name: “Tom Ta” roles: [“Ruby on Rails Developer”,

    “Amateur Speaker”] works_at: “Futureworkz” twitter: “@taminhtien” github: [“/taminhtien”, “/tomfwz”] }
  2. What is Metaprogramming? • Feel free to define methods and

    classes in run time • Make your application more DRY, lightweight, intuitive, extendable, and more maintainable • Code that writes code, make your code more concise, reduce duplicate code • Code that treats code as data
  3. class CarModel def engine_info=(info) @engine_info = info end def engine_info

    @engine_info end def engine_price=(price) @engine_price = price end def engine_price @engine_price end def wheel_info=(info) @wheel_info = info end def wheel_info @wheel_info end def wheel_price=(price) @wheel_price = price end def wheel_price @wheel_price end def airbag_info=(info) @airbag_info = info end def airbag_info @airbag_info end def airbag_price=(price) @airbag_price = price end def airbag_price @airbag_price end def alarm_info=(info) @alarm_info = info end def alarm_info @alarm_info end def alarm_price=(price) @alarm_price = price end def alarm_price @alarm_price end def stereo_info=(info) @stereo_info = info end def stereo_info @stereo_info end def stereo_price=(price) @stereo_price = price end def stereo_price @stereo_price end end
  4. class CarModel FEATURES = ["engine", "wheel", "airbag", "alarm", "stereo"] FEATURES.each

    do |feature| define_method("#{feature}_info=") do |info| instance_variable_set("@#{feature}_info", info) end define_method("#{feature}_info") do instance_variable_get("@#{feature}_info") end define_method "#{feature}_price=" do |price| instance_variable_set("@#{feature}_price", price) end define_method("#{feature}_price") do instance_variable_get("@#{feature}_price") end end end
  5. class CarModel attr_accessor :engine_info, :engine_price, :wheel_info, :wheel_price, :airbag_info, :airbag_price, :alarm_info,

    :alarm_price, :stereo_info, :stereo_price end class CarModel def self.features(*args) args.each do |feature| attr_accessor "#{feature}_price", "#{feature}_info" end end features :engine, :wheel, :airbag, :alarm, :stereo end
  6. Ruby Object Model • Object • Class • Class Methods

    • Instance Methods • Singleton Classes • Singleton Methods
  7. Objects • Everything in Ruby is an Object module Magic

    attr_accessor :magic_points def cast(spell) “Your cast #{spell}” end end class Wizard < Character include Magic attr_accessor :name def initialize(options = {}) @name = options.fetch :name, ‘NPC’ @hit_points = rand(1..4) end end
  8. gandalf = Wizard.new name: 'Gandalf' => #<Wizard:0x007fbea4a3cea0 @name="Gandalf"> gandalf.class =>

    Wizard gandalf.class.superclass => Character gandalf.class.superclass.superclass => Object gandalf.class.superclass.superclass.superclass => BasicObject gandalf.class.ancestors => [Wizard, Magic, Character, Object, Kernel, BasicObject]
  9. class Wizard < Character include Magic attr_accessor :name def initialize(options

    = {}) @name = options.fetch :name, ‘NPC’ @hit_points = rand(1..4) end end gandalf.name #=> "Gandalf" gandalf.hit_points #=> 3 gandalf.cast('fireball') #=> "Gandalf casts fireball" Calling method Where are these? module Magic attr_accessor :magic_points def cast(spell) “Your cast #{spell}” end end
  10. Not here Where is #cast? Not here too It’s here

    module Magic attr_accessor :magic_points def cast(spell) “Your cast #{spell}” end end
  11. class Wizard < Character include Magic attr_accessor :name def initialize(options

    = {}) @name = options.fetch :name, 'NPC' @hit_points = rand(1..4) end end gandalf # => #<Wizard:0x007fbea4b2f3d0 @name="Gandalf", @hit_points=1> < Object < Basic Object First, look at the instance Then, look in the class’s methods Next, find in included module Then, go to the superclass Continue until BasicObject Last in, first out
  12. :singleton_methods Methods that are available only for a specific object

    class Foo end foo = Foo.new def foo.shout puts "Foo Foo Foo!" end foo.shout # => "Foo Foo Foo!" p Foo.new.respond_to?(:shout) # => false
  13. :singleton_classes When you declare a singleton method on an object,

    Ruby automatically creates a class to hold just that method foo Foo’ (singleton) Foo super super
  14. Accessing the singleton class class Object def singleton_method?(method) singleton_methods =

    self.singleton_class.instance_methods - self.class.instance_methods singleton_methods.include? method end end foo = "A string" def foo.shout puts foo.upcase end # shout is a singleton method. p foo.singleton_method?(:shout) # ⇒ true
  15. :class_methods • Methods that are beeing defined in the context

    of Class itself, not its instances. • Class methods are just singleton methods of a Class class Item def self.show puts “Class method show invoked” end end Item.show # => “Class method show invoked”
  16. :method_missing class Wizard < Character include Magic attr_accessor :name def

    initialize(options = {}) @name = options.fetch :name, ‘NPC’ @hit_points = rand(1..4) end end tom = Wizard.new tom.awesome_method
  17. Nope Where is #method_missing? Not here Nope Nope Eureka! #NoMethodError:

    undefine method ‘awesome_method’ for #<Wizard: 0x007f90b3997690> Not here, too
  18. class MyGhostClass def method_missing(name, *args) puts "#{name} was called with

    arguments: #{args.join(', ')}" end end m = MyGhostClass.new m.awesome_method("one", "two") # => awesome_method was called with arguments: one, two m.another_method("three", "four") # => another_method was called with arguments: three, four :method_missing
  19. :respond_to? Tell us if an object responds to a given

    method class Proxy def initialize(subject) @subject = subject end def method_missing(method) @subject.send(method) end end proxy = Proxy.new(Time) proxy.respond_to?(:now) # => false proxy.now # => Fri Feb 05 00:34:53 -0500 2010
  20. class Proxy def initialize(subject) @subject = subject end def method_missing(method)

    if @subject.respond_to?(method) @subject.send(method) else super(method) end end def respond_to?(method, include_private = false) super || @subject.respond_to?(method, include_private) end end proxy = Proxy.new(Time) proxy.respond_to?(:now) # => true proxy.now # => Fri Feb 05 00:34:53 -0500 2010
  21. :send class Timeline def initialize(tweets) @tweets = tweets end def

    contents @tweets end private def direct_messages end end tweets = ['Compiling!', 'Bundling...'] timeline = Timeline.new(tweets) timeline.contents timeline.send(:contents) timeline.send(“contents”) timeline.send(:direct_messages) send can run private or protected methods
  22. :public_send class Timeline def initialize(tweets) @tweets = tweets end def

    contents @tweets end private def direct_messages end end tweets = ['Compiling!', 'Bundling...'] timeline = Timeline.new(tweets) timeline.send(:direct_messages) send can run private or protected methods timeline.public_send(:direct_messages) private method `direct_messages' called for #<Timeline: 0x007fd273904eb0> (NoMethodError) prevents running private or protected methods
  23. :call class Timeline def initialize(tweets) @tweets = tweets end def

    contents @tweets end end tweets = ['Compiling!', 'Bundling...'] timeline = Timeline.new(tweets) content_method = timeline.method(:contents) content_method.call ⇒ #<Method: Timeline#contents> ⇒ ["Compiling!", "Bundling..."]
  24. :block • A bit of code that can be executed

    • Block syntax uses either do..end or curly braces ({}) [1, 2, 3].each do |num| puts num end # => Prints 1, 2, 3 on separate lines [1, 2, 3].each { |num| puts num } # => Prints 1, 2, 3 on separate lines
  25. :yield • Why do some methods accept a block and

    others don't? def block_test puts "We're in the method!" puts "Yielding to the block..." yield puts "We're back in the method!" end block_test { puts ">>> We're in the block!" } We're in the method! Yielding to the block... >>> We're in the block! We're back in the method! • It's because methods that accept blocks have a way of transferring control from the calling method to the block and back again.
  26. :monkey_patching • Every classes in Ruby are Open Classes •

    Can change classes at anytime, anywhere. • Can override any existed methods
  27. :monkey_patching p 'Tom Ta'.upcase # ⇒ “TOM TA” class String

    def upcase self.reverse end end p 'Tom Ta'.upcase # => “aT moT” Last def wins
  28. :define_method class Tweet def draft @status = :draft end def

    posted @status = :posted end def deleted @status = :deleted end end class Tweet states = [:draft, :posted, :deleted] states.each do |status| define_method status do @status = status end end end
  29. :alias_method class Timeline def initialize(tweets = []) @tweets = tweets

    end def tweets @tweets end def contents @tweets end end class Timeline def initialize(tweets = []) @tweets = tweets end def tweets @tweets end alias_method :contents, :tweets end
  30. :class_eval(block) • Works on the class as if you were

    opening it in a text editor and adding new state or behaviour to it. • Excutes block in context of the class class Tweet attr_accessor :status, :created_at def initialize(status) @status = status @created_at = Time.now end end Tweet.class_eval do attr_accessor :user end tweet = Tweet.new("Learning class_eval") tweet.user = "tom"
  31. :class_eval(string) class Monk def zen=(magic) @zen = magic end end

    old = Monk.new Monk.class_eval("def initialize; @zen = 42; end") Monk.class_eval("def zen; @zen; end") less_old = Monk.new puts "from the old instance: #{old.zen}" # => from the old instance: puts "from the new instance: #{less_old.zen}" # => from the new instance: 42 old.zen = 43 puts "from the old instance, with @zen set: #{old.zen}" # => from the old instance, with @zen set: 43 Code as data
  32. :instance_eval Excutes block in context of the instance class Tweet

    attr_accessor :user, :status end tweet = Tweet.new tweet.instance_eval do self.status = “Changing the tweet’s status” end Inside the block, self is the Tweet instance
  33. :instance_eval class Tweet attr_accessor :user, :status def initialize yield self

    if block_given? end end Tweet.new do |tweet| tweet.status = "Learning instance_eval" tweet.user = "Tom" end class Tweet attr_accessor :user, :status def initialize(&block) instance_eval(&block) if block_given? end end Tweet.new do self.status = "Learning instance_eval" self.user = "Tom" end
  34. :extend module Searchable def find_all_from(user) end end class Tweet extend

    Searchable end Tweet.find_all_from(‘tom’) use extend to expose methods as class methods
  35. :include module ImageUtils def preview end def transfer(destination) end end

    class Image include ImageUtils end image = user.image image.preview use include to expose methods as instance methods
  36. :self.included module ImageUtils def preview end def transfer(destination) end module

    ClassMethods def fetch_from_twitter(user) end end end class Image include ImageUtils extend ImageUtils::ClassMethods end image = user.image image.preview Image.fetch_from_twitter(‘tom’)
  37. :self.included module ImageUtils def self.included(base) base.extend(ClassMethods) end def preview end

    def transfer(destination) end module ClassMethods def fetch_from_twitter(user) end end end class Image include ImageUtils extend ImageUtils::ClassMethods end image = user.image image.preview Image.fetch_from_twitter(‘tom’) self.included is called by Ruby when a module is included in a class
  38. :self.included module ImageUtils def self.included(base) base.extend(ClassMethods) end def preview end

    def transfer(destination) end module ClassMethods def fetch_from_twitter(user) end end end class Image include ImageUtils end image = user.image image.preview Image.fetch_from_twitter(‘tom’) base is Image Class