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

Metaprogramming with super powers

Metaprogramming with super powers

Metaprogramming in Ruby gets a bad rap, yet Rails itself is chock-full of metaprogramming. So what is it that separates good metaprogramming from bad? Here we will take a look at some of the problems people encounter when they attempt to wield it themselves, and approaches that can make your own metaprogramming code feel as magical as it feels in Rails.

Bradley Schaefer

February 14, 2018
Tweet

More Decks by Bradley Schaefer

Other Decks in Programming

Transcript

  1. UTC = ActiveSupport::TimeZone["UTC"] class Datapoint < ApplicationRecord def datapoint_at(tz =

    UTC) tz.at(datapoint_ts) end end class ChartAnnotation < ApplicationRecord def annotation_at(tz = UTC) tz.at(annotation_ts) end end
  2. UTC = ActiveSupport::TimeZone["UTC"] class Datapoint < ApplicationRecord def datapoint_at(tz =

    UTC) tz.at(datapoint_ts) end end class ChartAnnotation < ApplicationRecord def annotation_at(tz = UTC) tz.at(annotation_ts) end end
  3. UTC = ActiveSupport::TimeZone["UTC"] class Datapoint < ApplicationRecord def datapoint_at(tz =

    UTC) tz.at(datapoint_ts) end end class ChartAnnotation < ApplicationRecord def annotation_at(tz = UTC) tz.at(annotation_ts) end end
  4. UTC = ActiveSupport::TimeZone["UTC"] class Datapoint < ApplicationRecord def datapoint_at(tz =

    UTC) tz.at(datapoint_ts) end end class ChartAnnotation < ApplicationRecord def annotation_at(tz = UTC) tz.at(annotation_ts) end end
  5. module Timify def timify(attr) at_attr = attr.to_s.chomp("ts") + "at" define_method(at_attr)

    do |tz = UTC| tz.at(public_send(attr)) end end end class Datapoint extend Timify timify :datapoint_ts def datapoint_at(tz = UTC) tz.at(datapoint_ts) end end
  6. module Timify def timify(attr) at_attr = attr.to_s.chomp("ts") + "at" define_method(at_attr)

    do |tz = UTC| tz.at(public_send(attr)) end end end class Datapoint extend Timify timify :datapoint_ts def datapoint_at(tz = UTC) tz.at(datapoint_ts) end end
  7. module Timify def timify(attr) at_attr = attr.to_s.chomp("ts") + "at" define_method(at_attr)

    do |tz = UTC| tz.at(public_send(attr)) end end end class Datapoint extend Timify timify :datapoint_ts end
  8. class Datapoint extend Timify timify :datapoint_ts end •"timify" is a

    verb, it's doing something to "datapoint_ts" - but what? •"datapoint_at" method comes out of nowhere •subtle problem with overriding
  9. class Datapoint extend Timify timify :datapoint_ts end •"timify" is a

    verb, it's doing something to "datapoint_ts" - but what? •"datapoint_at" method comes out of nowhere •subtle problem with overriding
  10. class Datapoint extend Timify timify :datapoint_ts end •"timify" is a

    verb, it's doing something to "datapoint_ts" - but what? •"datapoint_at" method comes out of nowhere •subtle problem with overriding
  11. Principle of Least Astonishment “People are part of the system.

    The design should match the user's experience, expectations, and mental models” –Principles of computer system design: an introduction ISBN 978-0-12-374957-4
  12. module TimeAttributeMethods def time_reader(attr, from:) define_method(attr) do |tz = UTC|

    tz.at(public_send(from)) end end end class Datapoint extend TimeAttributeMethods time_reader :datapoint_at, from: :datapoint_ts end
  13. module TimeAttributeMethods def time_reader(attr, from:) define_method(attr) do |tz = UTC|

    tz.at(public_send(from)) end end end class Datapoint extend TimeAttributeMethods time_reader :datapoint_at, from: :datapoint_ts end
  14. module TimeAttributeMethods def time_reader(attr, from:) define_method(attr) do |tz = UTC|

    tz.at(public_send(from)) end end end class Datapoint extend TimeAttributeMethods time_reader :datapoint_at, from: :datapoint_ts end
  15. class AlarmEvent extend TimeAttributeMethods time_reader :end_at, from: :end_ts def end_at(tz

    = UTC) return tz.now if end_ts.nil? super end end alarm_event.end_at # What happens?
  16. class AlarmEvent extend TimeAttributeMethods time_reader :end_at, from: :end_ts def end_at(tz

    = UTC) return tz.now if end_ts.nil? super end end alarm_event.end_at # What happens? NoMethodError: super: no superclass method `end_at' for #<AlarmEvent: 0x00007fba40d4cf40>
  17. module TimeAttributeMethods def time_reader(attr, from:) define_method(attr) do |tz = UTC|

    tz.at(public_send(from)) end end end time_reader :end_at, from: :end_ts def end_at(tz = UTC) tz.at(end_ts) end def end_at(tz = UTC) return tz.now if end_ts.nil? super end
  18. module TimeAttributeMethods def time_reader(attr, from:) define_method(attr) do |tz = UTC|

    tz.at(public_send(from)) end end end time_reader :end_at, from: :end_ts def end_at(tz = UTC) tz.at(end_ts) end def end_at(tz = UTC) return tz.now if end_ts.nil? super end
  19. module TimeAttributeMethods def time_reader(attr, from:) define_method(attr) do |tz = UTC|

    tz.at(public_send(from)) end end end time_reader :end_at, from: :end_ts def end_at(tz = UTC) tz.at(end_ts) end def end_at(tz = UTC) return tz.now if end_ts.nil? super end
  20. module TimeAttributeMethods def time_reader(attr, from:) define_method(attr) do |tz = UTC|

    tz.at(public_send(from)) end end end time_reader :end_at, from: :end_ts def end_at(tz = UTC) tz.at(end_ts) end def end_at(tz = UTC) return tz.now if end_ts.nil? super end
  21. module TimeAttributeMethods def time_reader(attr, from:) define_method(attr) do |tz = UTC|

    tz.at(public_send(from)) end end end time_reader :end_at, from: :end_ts def end_at(tz = UTC) tz.at(end_ts) end def end_at(tz = UTC) return tz.now if end_ts.nil? super end
  22. module TimeAttributeMethods def time_reader(attr, from:) include (Module.new do |mod| define_method(attr)

    do |tz = UTC| tz.at(public_send(from)) end end) end end time_reader :datapoint_at, from: :datapoint_ts Datapoint.ancestors # => [Datapoint, #<Module: 0x00007f9381b73ce0>, Object, PP::ObjectMixin, Kernel, BasicObject]
  23. time_reader :end_at, from: :end_ts def end_at(tz = UTC) return tz.now

    if end_ts.nil? super end AlarmEvent.ancestors # => [ AlarmEvent, #<Module:0x00007f9381b73ce0>, Object, Kernel, BasicObject ]
  24. time_reader :end_at, from: :end_ts def end_at(tz = UTC) return tz.now

    if end_ts.nil? super end AlarmEvent.ancestors # => [ AlarmEvent, #<Module:0x00007f9381b73ce0>, Object, Kernel, BasicObject ] super
  25. time_reader :end_at, from: :end_ts def end_at(tz = UTC) return tz.now

    if end_ts.nil? super end AlarmEvent.ancestors # => [ AlarmEvent, #<Module:0x00007f9381b73ce0>, Object, Kernel, BasicObject ] super
  26. pry(main)> ls Datapoint.new #<Module:0x00007fa65d1ef338>#methods: datapoint_at Datapoint#methods: datapoint_ts pry(main)> $ Datapoint.new.datapoint_at

    From: /Users/soulcutter/super-powers/ time_attribute_methods.rb @ line 4: Owner: #<Module:0x00007fa65d1ef338> Visibility: public Number of lines: 3 define_method(attr) do |tz = UTC| tz.at(public_send(from)) end
  27. pry(main)> ls Datapoint.new #<Module:0x00007fa65d1ef338>#methods: datapoint_at pry(main)> $ Datapoint.new.datapoint_at From: /Users/soulcutter/super-powers/

    time_attribute_methods.rb @ line 4: Owner: #<Module:0x00007fa65d1ef338> Visibility: public Number of lines: 3 define_method(attr) do |tz = UTC| tz.at(public_send(from)) end
  28. 1.Define a subclass of Module 2.Define an initializer that takes

    configuration 3.Use configuration to define methods
  29. class TimeAttribute < Module def initialize(attr, from) define_method(attr) do |tz

    = UTC| tz.at(public_send(from)) end end end module TimeAttributeMethods def time_reader(attr, from:) include TimeAttribute.new(attr, from) end end
  30. class TimeAttribute < Module def initialize(attr, from) define_method(attr) do |tz

    = UTC| tz.at(public_send(from)) end end end module TimeAttributeMethods def time_reader(attr, from:) include TimeAttribute.new(attr, from) end end
  31. class TimeAttribute < Module def initialize(attr, from) define_method(attr) do |tz

    = UTC| tz.at(public_send(from)) end end end module TimeAttributeMethods def time_reader(attr, from:) include TimeAttribute.new(attr, from) end end
  32. class TimeAttribute < Module def initialize(attr, from) define_method(attr) do |tz

    = UTC| tz.at(public_send(from)) end end end module TimeAttributeMethods def time_reader(attr, from:) include TimeAttribute.new(attr, from) end end
  33. pry(main)> ls Datapoint.new #<TimeAttribute:0x00007fe81cbe3a00>#methods: datapoint_at Datapoint#methods: datapoint_ts pry(main)> $ Datapoint.new.datapoint_at

    From: /Users/soulcutter/super-powers/ time_attribute_methods.rb @ line 4: Owner: #<Module:0x00007fa65d1ef338> Visibility: public Number of lines: 3 define_method(attr) do |tz = UTC| tz.at(public_send(from))
  34. def inspect "#<#{self.class.name}: attr=#{@attr.inspect} \ from=#{@from.inspect}>" end pry(main)> ls Datapoint.new

    #<TimeAttribute: attr=:datapoint_at from=:datapoint_ts>#methods: datapoint_at Datapoint#methods: datapoint_ts
  35. def inspect "#<#{self.class.name}: attr=#{@attr.inspect} \ from=#{@from.inspect}>" end pry(main)> ls Datapoint.new

    #<TimeAttribute: attr=:datapoint_at from=:datapoint_ts>#methods: datapoint_at Datapoint#methods: datapoint_ts