Metaprogramming for Generalists

Metaprogramming for Generalists

05fdba1ae381f24512e977f8fe2697b4?s=128

Chris Salzberg

August 24, 2018
Tweet

Transcript

  1. FOR GENERALISTS metaprogramming CHRIS SALZBERG

  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.
  3. 1. generalist metaprogramming 2. building generic software

  4. 1 generalist metaprogramming

  5. metaprogramming

  6. metaprogramming (noun)

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

  8. metaprogramming (noun) 1. writing code that writes code 2. technique

    in which computer programs have the ability to treat programs as their data
  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
  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
  11. then

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

    method_added method send
  13. (Normal) PROGRAMMING metaprogramming

  14. None
  15. ruby ruby

  16. ruby ruby applications applications

  17. ruby ruby applications applications libraries libraries

  18. ruby ruby applications applications metaprogramming lives here libraries libraries

  19. why?

  20. generalization

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

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

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

    abstracting common properties.
  24. attribute

  25. knockout experiment

  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
  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
  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
  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
  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
  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
  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
  33. application library control knockout

  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
  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
  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")
  37. would you use this?

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

    speaking")
  39. metaprogramming levels the playing field by enabling libraries to translate

    unknowns into code
  40. eval("foo")

  41. eval("foo") foo

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

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

  44. attr_writer "foo"

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

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

  47. def define_writer(name) attr_writer name end

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

  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"
  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"
  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
  52. (meta) PROGRAMMING “normal” programming

  53. (meta) PROGRAMMING generic software

  54. 2 building generic software

  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
  56. equality

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

  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
  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
  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
  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
  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
  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
  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
  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)
  67. class Address def ==(other) [:street, :city, :country].all? { |key| other.send(key)

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

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

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

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

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

    |other| keys.all? { |key| other.send(key) == send(key) } end end method body
  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
  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
  75. require "equalizer" class Address extend Equalizer equalize :street, :city, :country

    end
  76. generic softw Equalizer Address

  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
  78. module Equalizer def equalize(*keys) define_method :== do |other| keys.all? {

    |key| other.send(key) == send(key) } end end end
  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 #=> #<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" >
  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 #=> { #<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
  81. None
  82. None
  83. To be a generalist

  84. To be a generalist generalist

  85. To be a generalist generalist

  86. To be a generalist generalists

  87. Chris Salzberg @shioyama / dejimata.com

  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