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

Metaprogramming for Generalists

Metaprogramming for Generalists

Chris Salzberg

August 24, 2018
Tweet

More Decks by Chris Salzberg

Other Decks in Programming

Transcript

  1. FOR GENERALISTS
    metaprogramming
    CHRIS SALZBERG

    View Slide

  2. 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.

    View Slide

  3. 1. generalist metaprogramming
    2. building generic software

    View Slide

  4. 1
    generalist
    metaprogramming

    View Slide

  5. metaprogramming

    View Slide

  6. metaprogramming (noun)

    View Slide

  7. metaprogramming (noun)
    1. writing code that writes code

    View Slide

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

    View Slide

  9. 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

    View Slide

  10. 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

    View Slide

  11. then

    View Slide

  12. define_method
    Class.new
    class_eval
    instance_eval
    eval
    method_missing
    Module.new
    binding
    arity
    parameters
    method_added
    method
    send

    View Slide

  13. (Normal)
    PROGRAMMING
    metaprogramming

    View Slide

  14. View Slide

  15. ruby
    ruby

    View Slide

  16. ruby
    ruby
    applications
    applications

    View Slide

  17. ruby
    ruby
    applications
    applications
    libraries
    libraries

    View Slide

  18. ruby
    ruby
    applications
    applications
    metaprogramming
    lives here
    libraries
    libraries

    View Slide

  19. why?

    View Slide

  20. generalization

    View Slide

  21. generalization (noun)
    Formulation of general concepts from specific
    instances by abstracting common properties.

    View Slide

  22. generalization (noun)
    Formulation of general concepts from specific
    instances by abstracting common properties.

    View Slide

  23. generalization (noun)
    Formulation of general concepts from specific
    instances by abstracting common properties.

    View Slide

  24. attribute

    View Slide

  25. knockout experiment

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide

  30. 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

    View Slide

  31. 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

    View Slide

  32. 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

    View Slide

  33. application
    library
    control knockout

    View Slide

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

    View Slide

  35. 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

    View Slide

  36. talk = Talk.find(...)
    first_name = talk.get_association(:speaker)
    .get_attribute(:first_name)
    comment = talk.get_association(:comments).build
    comment.set_attribute(
    :content, "#{first_name} is speaking")

    View Slide

  37. would you use this?

    View Slide

  38. talk = Talk.find(...)
    comment = talk.comments.build
    comment.content =
    "#{talk.speaker.first_name} is speaking")

    View Slide

  39. metaprogramming levels the playing field
    by enabling libraries to
    translate unknowns into code

    View Slide

  40. eval("foo")

    View Slide

  41. eval("foo")
    foo

    View Slide

  42. str = "..."
    eval("foo#{str}")

    View Slide

  43. def fooval(str)
    eval("foo#{str}")
    end

    View Slide

  44. attr_writer "foo"

    View Slide

  45. attr_writer "foo"
    def foo=(value)
    @foo = value
    end

    View Slide

  46. def define_writer(name)
    define_method("#{name}=") do |value|
    instance_variable_set(:"@#{name}",
    value)
    end
    end

    View Slide

  47. def define_writer(name)
    attr_writer name
    end

    View Slide

  48. control
    control
    contains metaprogramming method(s)
    knockout
    knockout

    View Slide

  49. 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"

    View Slide

  50. 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"

    View Slide

  51. 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

    View Slide

  52. (meta)
    PROGRAMMING
    “normal” programming

    View Slide

  53. (meta)
    PROGRAMMING
    generic software

    View Slide

  54. 2
    building generic
    software

    View Slide

  55. 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

    View Slide

  56. equality

    View Slide

  57. View Slide

  58. λ ~/dev/rails/ master ag "def ==[^\=]" -l

    View Slide

  59. λ ~/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

    View Slide

  60. λ ~/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

    View Slide

  61. 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

    View Slide

  62. 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

    View Slide

  63. 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

    View Slide

  64. 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

    View Slide

  65. 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

    View Slide

  66. 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)

    View Slide

  67. class Address
    def ==(other)
    [:street, :city, :country].all? { |key|
    other.send(key) == send(key)
    }
    end
    end
    abstraction

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  73. 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

    View Slide

  74. 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

    View Slide

  75. require "equalizer"
    class Address
    extend Equalizer
    equalize :street, :city, :country
    end

    View Slide

  76. generic softw
    Equalizer
    Address

    View Slide

  77. 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

    View Slide

  78. module Equalizer
    def equalize(*keys)
    define_method :== do |other|
    keys.all? { |key|
    other.send(key) == send(key)
    }
    end
    end
    end

    View Slide

  79. 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
    #=> #city="Wien" address="Austria" >
    euruko2018 = Address.new("Neubaugürtel 34-36",
    "Wein", "Austria")
    euruko2018.inspect
    #=> #city="Wien" address="Austria" >

    View Slide

  80. 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
    #=> { #=>2 }
    point_a = GpsLocation.new(1, 2)
    point_b = GpsLocation.new(1, 2)
    visits = {}
    visits[point_a] = 1
    visits[point_b] = 2
    visits
    #=> { #=>2 }
    build on abstraction

    View Slide

  81. View Slide

  82. View Slide

  83. To be a generalist

    View Slide

  84. To be a generalist
    generalist

    View Slide

  85. To be a generalist
    generalist

    View Slide

  86. To be a generalist
    generalists

    View Slide

  87. Chris Salzberg
    @shioyama / dejimata.com

    View Slide

  88. 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

    View Slide