Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Metaprogramming for Generalists

Metaprogramming for Generalists

Chris Salzberg

August 24, 2018
Tweet

More Decks by Chris Salzberg

Other Decks in Programming

Transcript

  1. about me • My handle is @shioyama. • I live

    in Tokyo, Japan. • I’m a Canadian from Montréal. • I work at a company called Degica. • I’m the author of a gem called Mobility. • I blog at dejimata.com.
  2. metaprogramming (noun) 1. writing code that writes code 2. technique

    in which computer programs have the ability to treat programs as their data
  3. metaprogramming (noun) 1. writing code that writes code 2. technique

    in which computer programs have the ability to treat programs as their data 3. a way for a program to find out things about itself, or other programs
  4. metaprogramming (noun) 1. writing code that writes code 2. technique

    in which computer programs have the ability to treat programs as their data 3. a way for a program to find out things about itself, or other programs 4. a bag of tricks
  5. class Model def initialize; @attributes = {}; end def self.define_attribute(name)

    define_method name do @attributes.fetch(name) end define_method "#{name}=" do |value| @attributes[name] = value end end end class Model def initialize; @attributes = {}; end def self.define_attribute(name) define_method name do @attributes.fetch(name) end define_method "#{name}=" do |value| @attributes[name] = value end end end control
  6. class Talk < Model define_attribute :title define_attribute :abstract end talk

    = Talk.new talk.title = "Metaprogramming for Generalists" talk.title #=> "Metaprogramming for Generalists" talk.abstract = "It conjures up images of..." talk.abstract #=> "It conjures up images of..." control
  7. class Talk < Model define_attribute :title define_attribute :abstract end talk

    = Talk.new talk.title = "Metaprogramming for Generalists" talk.title #=> "Metaprogramming for Generalists" talk.abstract = "It conjures up images of..." talk.abstract #=> "It conjures up images of..." abstraction is transparent control
  8. class Model def initialize; @attributes = {}; end def get_attribute(name)

    @attributes.fetch(name) end def set_attribute(name, value) @attributes[name] = value end end class Model def initialize; @attributes = {}; end def get_attribute(name) @attributes.fetch(name) end def set_attribute(name, value) @attributes[name] = value end end knockout
  9. class Talk < Model end talk = Talk.new talk.set_attribute(:title, "MP

    for Generalists") talk.get_attribute(:title) #=> "MP for Generalists" talk.set_attribute(:abstract, "It conjures...") talk.get_attribute(:abstract) #=> "It conjures..." knockout
  10. class Talk < Model end talk = Talk.new talk.set_attribute(:title, "MP

    for Generalists") talk.get_attribute(:title) #=> "MP for Generalists" talk.set_attribute(:abstract, "It conjures...") talk.get_attribute(:abstract) #=> "It conjures..." abstraction is visible knockout
  11. class Talk < Model end talk = Talk.new talk.set_attribute(:title, "MP

    for Generalists") talk.get_attribute(:title) #=> "MP for Generalists" talk.set_attribute(:abstract, "It conjures...") talk.get_attribute(:abstract) #=> "It conjures..." knockout names are arguments
  12. application library control knockout - abstraction invisible - send as

    message - speaks in language of domain - abstraction visible - send as argument - speaks in language of abstraction
  13. application library control knockout - abstraction invisible - send as

    message - speaks in language of domain - abstraction visible - send as argument - speaks in language of abstraction - unknowns refer to code - hard to understand - unknowns do not refer to code - easier to understand
  14. control control contains metaprogramming method(s) knockout knockout define_writer define_writer[2] [2]

    define_writer define_writer[1] [1] fooval fooval attr_writer "foo" attr_writer "foo" eval "foo" eval "foo"
  15. control control contains metaprogramming method(s) knockout knockout define_writer define_writer[2] [2]

    define_writer define_writer[1] [1] fooval fooval attr_writer "foo" attr_writer "foo" not reducible to “normal” programming eval "foo" eval "foo"
  16. begin module rescue def nil false class do case self

    @foo true end for while if else ensure until “normal programming” = cannot convert unknowns into code
  17. Jeremy Evans One of the best ways to write flexible

    software is to write generic software. Instead of designing a single API that completely handles a specific case, you write multiple APIs that handle smaller, more generic parts of that use case and then handling the entire case is just gluing those parts together.” “ “The Development of Sequel”, May 2012
  18. λ ~/dev/rails/ master ag "def ==[^\=]" -l activejob/test/cases/serializers_test.rb activejob/test/models/person.rb activemodel/lib/active_model/attribute_set.rb

    activemodel/lib/active_model/attribute_set/builder.rb activemodel/lib/active_model/attribute.rb activemodel/lib/active_model/type/value.rb activemodel/lib/active_model/type/binary.rb actionpack/lib/action_dispatch/middleware/stack.rb actionpack/lib/action_dispatch/http/mime_type.rb actionpack/lib/action_controller/metal/strong_parameters.rb actionview/lib/action_view/template/types.rb activesupport/lib/active_support/duration.rb activerecord/test/models/customer.rb activerecord/lib/active_record/associations/collection_proxy.rb activerecord/lib/active_record/relation.rb activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb activerecord/lib/active_record/connection_adapters/postgresql/utils.rb activerecord/lib/active_record/connection_adapters/column.rb activerecord/lib/active_record/core.rb activerecord/lib/active_record/relation/where_clause.rb activerecord/lib/active_record/reflection.rb activerecord/lib/active_record/association_relation.rb activerecord/lib/active_record/aggregations.rb
  19. λ ~/dev/rails/ master ag "def ==[^\=]" -l activejob/test/cases/serializers_test.rb activejob/test/models/person.rb activemodel/lib/active_model/attribute_set.rb

    activemodel/lib/active_model/attribute_set/builder.rb activemodel/lib/active_model/attribute.rb activemodel/lib/active_model/type/value.rb activemodel/lib/active_model/type/binary.rb actionpack/lib/action_dispatch/middleware/stack.rb actionpack/lib/action_dispatch/http/mime_type.rb actionpack/lib/action_controller/metal/strong_parameters.rb actionview/lib/action_view/template/types.rb activesupport/lib/active_support/duration.rb activerecord/test/models/customer.rb activerecord/lib/active_record/associations/collection_proxy.rb activerecord/lib/active_record/relation.rb activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb activerecord/lib/active_record/connection_adapters/postgresql/utils.rb activerecord/lib/active_record/connection_adapters/column.rb activerecord/lib/active_record/core.rb activerecord/lib/active_record/relation/where_clause.rb activerecord/lib/active_record/reflection.rb activerecord/lib/active_record/association_relation.rb activerecord/lib/active_record/aggregations.rb
  20. class Address attr_reader :street, :city, :country def initialize(street, city, country)

    @street, @city, @country = street, city, country end # ... def ==(other) other.is_a?(self.class) && other.street == street && other.city == city && other.country == country end end # activerecord/test/models/customer.rb:15
  21. class GpsLocation attr_reader :gps_location # ... def latitude gps_location.split("x").first end

    def longitude gps_location.split("x").last end def ==(other) other.latitude == latitude && other.longitude == longitude end end # activerecord/test/models/customer.rb:45
  22. class Address def ==(other) other.is_a?(self.class) && other.street == street &&

    other.city == city && other.country == country end end class GpsLocation def ==(other) other.latitude == latitude && other.longitude == longitude end end
  23. class Address def ==(other) other.street == street && other.city ==

    city && other.country == country end end class GpsLocation def ==(other) other.latitude == latitude && other.longitude == longitude end end
  24. class Address def ==(other) other.send(:street) == send(:street) && other.send(:city) ==

    send(:city) && other.send(:country) == send(:country) end end class GpsLocation def ==(other) other.send(:latitude) == send(:latitude) && other.send(:longitude) == send(:longitude) end end
  25. class Address def ==(other) [:street, :city, :country].all? { |key| other.send(key)

    == send(key) } end end other.send(:street) == send(:street) && other.send(:city) == send(:city) && other.send(:country) == send(:country)
  26. class Address def ==(other) keys = [:street, :city, :country] keys.all?

    { |key| other.send(key) == send(key) } end end
  27. class Address define_method :== do |other| keys = [:street, :city,

    :country] keys.all? { |key| other.send(key) == send(key) } end end
  28. class Address keys = [:street, :city, :country] define_method :== do

    |other| keys.all? { |key| other.send(key) == send(key) } end end
  29. class Address keys = [:street, :city, :country] define_method :== do

    |other| keys.all? { |key| other.send(key) == send(key) } end end method arguments
  30. class Address keys = [:street, :city, :country] define_method :== do

    |other| keys.all? { |key| other.send(key) == send(key) } end end method body
  31. class Address def self.equalize(*keys) define_method :== do |other| keys.all? {

    |key| other.send(key) == send(key) } end end equalize :street, :city, :country end
  32. module Equalizer def equalize(*keys) define_method :== do |other| keys.all? {

    |key| other.send(key) == send(key) } end end end class Address extend Equalizer equalize :street, :city, :country end unknowns
  33. class Address extend Equalizer equalize :street, :city, :country end class

    Address extend Equalizer equalize :street, :city, :country end class GpsLocation extend Equalizer equalize :latitude, :longitude end class GpsLocation extend Equalizer equalize :latitude, :longitude end require "equalizer" class AM::Type::Value extend Equalizer equalize :precision, :scale, :limit end class AM::Type::Value extend Equalizer equalize :precision, :scale, :limit end class AR::Attribute extend Equalizer equalize :name, :value_before_type_cast, :type end class AR::Attribute extend Equalizer equalize :name, :value_before_type_cast, :type end class AM::AttributeSet extend Equalizer equalize :attributes end class AM::AttributeSet extend Equalizer equalize :attributes end class AR::ConnectionAdapters::Column extend Equalizer equalize :attributes_for_hash end class AR::ConnectionAdapters::Column extend Equalizer equalize :attributes_for_hash end class AM::AttributeSet::Builder extend Equalizer equalize :materialize end class AM::AttributeSet::Builder extend Equalizer equalize :materialize end class AR::Relation::WhereClause extend Equalizer equalize :predicates end class AR::Relation::WhereClause extend Equalizer equalize :predicates end
  34. module Equalizer def equalize(*keys) define_method :== do |other|; ... ;

    end define_method :inspect do "#<#{self.class.name}#{keys.map { |key| " #{key}=#{send(key).inspect} " }.join}>" end end end build on abstraction euruko2018 = Address.new("Neubaugürtel 34-36", "Wein", "Austria") euruko2018.inspect #=> #<Address street="Neubaugürtel 34-36" city="Wien" address="Austria" > euruko2018 = Address.new("Neubaugürtel 34-36", "Wein", "Austria") euruko2018.inspect #=> #<Address street="Neubaugürtel 34-36" city="Wien" address="Austria" >
  35. module Equalizer def equalize(*keys) define_method :== do |other|; ... ;

    end define_method :inspect do; ... ; end define_method :hash do keys.map({ |key| send(key) }).hash end end end point_a = GpsLocation.new(1, 2) point_b = GpsLocation.new(1, 2) visits = {} visits[point_a] = 1 visits[point_b] = 2 visits #=> { #<GpsLocation lat=1 long=2>=>2 } point_a = GpsLocation.new(1, 2) point_b = GpsLocation.new(1, 2) visits = {} visits[point_a] = 1 visits[point_b] = 2 visits #=> { #<GpsLocation lat=1 long=2>=>2 } build on abstraction
  36. credits • Satellite image of Tokyo neighbourhood in self-intro used

    Google Maps • Visualization of dry-rb gems network created using Graph Commons (graphcommons.com) • Image of Ruby from: – http://pngimg.com/download/22155