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

[Saint-P RubyConf 2018] MetaCreativity

[Saint-P RubyConf 2018] MetaCreativity

Vladimir Dementyev

June 10, 2018
Tweet

More Decks by Vladimir Dementyev

Other Decks in Programming

Transcript

  1. palkan_tula palkan GOOD DSL 24 Brings simplicity & readability Hides

    complexity & internals Makes developers happy (through flexibility, testability, whatever-ability)
  2. palkan_tula palkan 25 feature "Log In" do background { visit

    "/" } scenario do fill_in 'Email', with: '[email protected]' fill_in 'Password', with: 'theforce' click_on 'Sign In' expect(page).to have_content 'Tatooine' end end
  3. palkan_tula palkan 27 class Memo ::Create < Trailblazer ::Operation step

    :create_model step :validate fail :assign_errors step :index pass :uuid step :save fail :log_errors # ... end
  4. palkan_tula palkan CREATIVITY 29 field :user, Types ::User, null: true

    def user BatchLoader .for(object.user_id) .batch do |ids, ldr| User.where(id: ds).each do |user| ldr.(user.id, user) end end end
  5. palkan_tula palkan CREATIVITY 31 ENV_CONSTANTS = %i[ENV].freeze def on_const(node) mod,

    klass = *node.children return unless (mod.nil? || mod.cbase_type?) add_offense( node.parent, :selector, MSG ) if ENV_CONSTANTS.include?(klass) end
  6. palkan_tula palkan CREATIVITY 32 def_node_matcher :env?, <<-PATTERN {(const nil? {:ENV})(const

    (cbase) {:ENV})} PATTERN def on_const(node) add_offense( node.parent, :selector, MSG ) if env?(node) end
  7. palkan_tula palkan 34 require "deferral/toplevel" using Deferral ::TopLevel def my_method_name

    file = File.open("my_file", "r") defer { file.close } file.write "data ..." end RUBY ❤ GO * https://github.com/tagomoris/deferral
  8. palkan_tula palkan 35 require_relative "./async_await" using AsyncAwait class A async

    def sleep! (rand * 2).tap do |t| sleep t end end end a = A.new p await a.sleep!, a.sleep! RUBY & JS * http://bit.ly/async-await-rb
  9. palkan_tula palkan 36 module FromStr def [](number) num = number.to_s.to_i

    return Result ::Err["Not a number", TypeError] unless num.to_s == number.to_s return Result ::Err["Negative", ArgumentError] if num.negative? return Result ::Err["Too big", ArgumentError] unless num < 64 Result ::Ok[num] end end FromStr["7"] # => Ok[7] .map { |x| x * 5 } # => Ok[35] .unwrap! # => 37 FromStr["foo"] # => Err["Not a number", TypeError] .map { |x| x * 5 } # => still Err["Not a number", TypeError] .unwrap! # => raises TypeError "Not a number" RUBY ❤ RUST * http://bit.ly/ruby-result
  10. palkan_tula palkan 43 Metaprogramming means to write a program that

    can, in turn, modify another program behaviour
  11. palkan_tula palkan INTROSPECTION 45 Retrieve information about a program being

    executed Not a metaprogramming itself but heavily used for
  12. palkan_tula palkan 55 To call a method is to “send”

    a message from one object to another DYNAMIC DISPATCH
  13. palkan_tula palkan 59 class A private def priv; end def

    method_missing(m, *args) p "Unknown: #{m}/ #{args.size}" end end A.new.priv # => "Unknown: priv/0”
  14. palkan_tula palkan 60 raise_method_missing(rb_execution_context_t *ec, int argc, const VALUE *argv,

    VALUE obj, enum method_missing_reason last_call_status) { if (last_call_status & MISSING_PRIVATE) { format = rb_fstring_cstr("private method `%s' called for %s%s%s"); } else if (last_call_status & MISSING_PROTECTED) { format = rb_fstring_cstr("protected method `%s' called for %s%s%s"); } else if (last_call_status & MISSING_SUPER) { format = rb_fstring_cstr("super: no superclass method `%s' for %s%s%s"); } exc = make_no_method_exception( exc, format, obj, argc, argv, last_call_status & (MISSING_FCALL | MISSING_VCALL)); rb_exc_raise(exc); } }
  15. palkan_tula palkan 62 class Phone def initialize(meta) @meta = meta

    end def method_missing(name, *args, &block) @meta[name] end end phone = Phone.new( type: "Eyephone", display: "MonkeyGlass") phone.type # => "Eyephone" phone.display # => nil CAVEAT
  16. palkan_tula palkan 63 module Names def self.clean(klass) ( klass.instance_methods -

    BasicObject.instance_methods - FORBIDDEN ).each { |m| klass.undef_method(m) } end end CLEAN CLASS https://github.com/evilmartians/evil-client/blob/master/lib/evil/client/names.rb
  17. palkan_tula palkan 65 class A def foo; "foo"; end end

    class B < A; def foo; "foob"; end end B.undef_method :foo B.new.foo # => NoMethoError undef vs remove
  18. palkan_tula palkan 66 undef vs remove class A def foo;

    "foo"; end end class B < A; def foo; "foob"; end end B.remove_method :foo B.new.foo # => foo
  19. palkan_tula palkan 68 require “benchmark" GC.disable class A def foo;

    "foo"; end def method_missing(_); "bar"; end end N = 1_000_000 a = A.new Benchmark.bm(45) do |x| x.report("defined") do N.times { a.foo } end x.report("missing") do N.times { a.bar } end end
  20. palkan_tula palkan 69 user system total real defined 0.161461 0.024387

    0.18584 (0.188324) missing 0.164019 0.023236 0.187255 (0.190112) RUBY 2.5.0 same-ish
  21. palkan_tula palkan 70 require “benchmark" GC.disable class A def foo;

    "foo"; end def method_missing(_); "bar"; end end methods = 1.upto(1_000_000).map { |n| :"foo #{n}" } a = A.new Benchmark.bm(45) do |x| x.report("defined") do methods.each { a.send(:foo) } end x.report(“missing diff”) do methods.each { |m| a.send(m) } end end
  22. palkan_tula palkan 71 user system total real defined 0.154720 0.024782

    0.179502 (0.180810) missing diff 0.463283 0.092282 0.555565 (0.559818) RUBY 2.5.0 ~3x slower
  23. palkan_tula palkan 75 def hi puts "hello, world" end puts

    RubyVM ::InstructionSequence .disasm(method(:hi)) ISeq
  24. palkan_tula palkan 76 $ ruby --dump=i -e "1 + 2”

    == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,5)>============== 0000 putobject_OP_INT2FIX_O_1_C_ ( 1)[Li] 0001 putobject 2 0003 opt_plus <callinfo!mid:+, argc:1, ARGS_SIMPLE> 0006 leave ISeq
  25. palkan_tula palkan 78 $ ruby --dump=i -e ' class A

    def foo; end end A.new.foo' == disasm: #<ISeq:<main>@-e:1 (1,0)-(6,9)>============== … 0003 defineclass :A, <class:A>, 0 … 0015 opt_send_without_block <callinfo!mid:new, argc:0>, <callcache> 0018 opt_send_without_block <callinfo!mid:foo, argc:0>, <callcache> 0021 leave METHOD DISPATCH
  26. palkan_tula palkan 79 DEFINE_INSN opt_send_without_block (CALL_INFO ci, CALL_CACHE cc) (

    ...) (VALUE val) // inc += -ci ->orig_argc; { struct rb_calling_info calling; calling.block_handler = VM_BLOCK_HANDLER_NONE; vm_search_method(ci, cc, calling.recv = TOPN(calling.argc = ci ->orig_argc)); CALL_METHOD(&calling, ci, cc); } METHOD DISPATCH
  27. palkan_tula palkan 80 static void vm_search_method(const struct rb_call_info *ci, struct

    rb_call_cache *cc, VALUE recv) { VALUE klass = CLASS_OF(recv); #if OPT_INLINE_METHOD_CACHE if (LIKELY(RB_DEBUG_COUNTER_INC_UNLESS(mc_global_state_miss, GET_GLOBAL_METHOD_STATE() == cc ->method_state) && RB_DEBUG_COUNTER_INC_UNLESS(mc_class_serial_miss, RCLASS_SERIAL(klass) == cc ->class_serial))) { /* cache hit! */ VM_ASSERT(cc ->call != NULL); RB_DEBUG_COUNTER_INC(mc_inline_hit); return; } RB_DEBUG_COUNTER_INC(mc_inline_miss); #endif cc ->me = rb_callable_method_entry(klass, ci ->mid); VM_ASSERT(callable_method_entry_p(cc ->me)); cc ->call = vm_call_general; #if OPT_INLINE_METHOD_CACHE cc ->method_state = GET_GLOBAL_METHOD_STATE(); cc ->class_serial = RCLASS_SERIAL(klass); #endif } METHOD DISPATCH
  28. palkan_tula palkan 81 static rb_method_entry_t * method_entry_get(VALUE klass, ID id,

    VALUE *defined_class_ptr) { #if OPT_GLOBAL_METHOD_CACHE struct cache_entry *ent; ent = GLOBAL_METHOD_CACHE(klass, id); if (ent ->method_state == GET_GLOBAL_METHOD_STATE() && ent ->class_serial == RCLASS_SERIAL(klass) && ent ->mid == id) { #if VM_DEBUG_VERIFY_METHOD_CACHE verify_method_cache(klass, id, ent ->defined_class, ent ->me); #endif if (defined_class_ptr) *defined_class_ptr = ent ->defined_class; RB_DEBUG_COUNTER_INC(mc_global_hit); #if DEBUG_CACHE_HIT debug_method_cache(1); #endif return ent ->me; } #endif RB_DEBUG_COUNTER_INC(mc_global_miss); #if DEBUG_CACHE_HIT debug_method_cache(0); #endif return method_entry_get_without_cache(klass, id, defined_class_ptr); } METHOD DISPATCH
  29. palkan_tula palkan 82 static rb_method_entry_t * method_entry_get_without_cache(VALUE klass, ID id,

    VALUE *defined_class_ptr) { VALUE defined_class; rb_method_entry_t *me = search_method(klass, id, &defined_class); if (UNDEFINED_METHOD_ENTRY_P(me)) { me = NULL; } return me; } static inline rb_method_entry_t* search_method(VALUE klass, ID id, VALUE *defined_class_ptr) { rb_method_entry_t *me; for (me = 0; klass; klass = RCLASS_SUPER(klass)) { if ((me = lookup_method_table(klass, id)) != 0) break; } if (defined_class_ptr) *defined_class_ptr = klass; return me; } METHOD DISPATCH
  30. palkan_tula palkan 85 module RubyDispatch refine BasicObject do def rd_send(mid,

    *args) meth = rd_search_method(self.class, mid) if meth.nil? return rd_call_no_method( self, mid, args ) end rd_call_method(self, meth, args) end end end ITER #0
  31. palkan_tula palkan 86 module RubyDispatch MISSING = :method_missing refine BasicObject

    do def rd_call_no_method(obj, mid, args) meth = rd_search_method( obj.class, MISSING ) args.unshift(mid) rd_call_method(obj, meth, args) end end end ITER #0
  32. palkan_tula palkan 87 module RubyDispatch refine BasicObject do def rd_call_method(obj,

    meth, args) meth.bind(obj).call(*args) end end end ITER #0
  33. palkan_tula palkan 88 class Square def area @side * @side

    end def initialize(side) @side = side end end area_un = Square.instance_method(:area) s = Square.new(5) area_un.bind(s).call # => 25 UnboundMethod
  34. palkan_tula palkan 90 module Eatable def eatable? humans_died_after_eaten_me.zero? end end

    class Food include Eatable def humans_died_after_eaten_me; 0; end end class Shawarma < Food def humans_died_after_eaten_me?; rand(4) / 2; end end Shawarma.new.eatable? LOOKUP
  35. palkan_tula palkan 94 Shawarma.ancestors # [ # Inedible, # Shawarma,

    # Food, # Eatable, # Object, # Kernel, # BasicObject # ] LOOKUP
  36. palkan_tula palkan include/prepend 95 Do no add methods to the

    target class Only affect lookup hierarchy
  37. palkan_tula palkan 96 def rd_search_method(klass, mid) iter = klass.ancestors.each kl

    = klass loop do return kl.instance_method(mid) if kl.instance_methods(false).include?(mid) || kl.private_instance_methods(false) .include?(mid) return if kl.eql?(BasicObject) kl = iter.next end end ITER #0
  38. palkan_tula palkan 97 require_relative "./ruby_dispatch" using RubyDispatch class A def

    foo; true; end def method_missing(mid) mid end end a = A.new p a.rd_send(:foo) # => true p a.rd_send(:bar) # => :bar ITER #0
  39. palkan_tula palkan 98 user system total real defined 0.002546 0.000231

    0.002777 (0.002775) missing 0.063106 0.009537 0.072643 (0.074051) ITER #0 ~30x slower
  40. palkan_tula palkan 99 static void vm_search_method(const struct rb_call_info *ci, struct

    rb_call_cache *cc, VALUE recv) { #if OPT_INLINE_METHOD_CACHE if (…) { /* cache hit! */ return; } #endif } METHOD DISPATCH
  41. palkan_tula palkan 100 $ ruby --dump=i -e ' class A

    def foo; end end A.new.foo' == disasm: #<ISeq:<main>@-e:1 (1,0)-(6,9)>============== … 0003 defineclass :A, <class:A>, 0 … 0015 opt_send_without_block <callinfo!mid:new, argc:0>, <callcache> 0018 opt_send_without_block <callinfo!mid:foo, argc:0>, <callcache> 0021 leave METHOD DISPATCH
  42. palkan_tula palkan 101 static rb_method_entry_t * method_entry_get(VALUE klass, ID id)

    { #if OPT_GLOBAL_METHOD_CACHE if(…) { #if DEBUG_CACHE_HIT debug_method_cache(1); #endif return ent ->me; } #endif #if DEBUG_CACHE_HIT debug_method_cache(0); #endif return method_entry_get_without_cache(klass, id); } METHOD DISPATCH
  43. palkan_tula palkan 102 static rb_method_entry_t * method_entry_get(VALUE klass, ID id)

    { #if OPT_GLOBAL_METHOD_CACHE if(…) { #if DEBUG_CACHE_HIT debug_method_cache(1); #endif return ent ->me; } #endif #if DEBUG_CACHE_HIT debug_method_cache(0); #endif return method_entry_get_without_cache(klass, id); } METHOD DISPATCH * NOTE: this is a custom patch
  44. palkan_tula palkan 103 # mcache_test.rb class A def foo; end

    def method_missing(mid, *); mid; end end a = A.new puts "\nDefined method\n\n" # Enable debug trace ENV['RUBY_DEBUG_METHOD_CACHE'] = '1' a.foo a.foo ENV['RUBY_DEBUG_METHOD_CACHE'] = '0' puts "\nMissing method\n\n" ENV['RUBY_DEBUG_METHOD_CACHE'] = '1' a.bar a.bar
  45. palkan_tula palkan 104 $ make runruby TESTRUN_SCRIPT=./mcache_test.rb Defined method [DEBUG

    METHOD CACHE] miss [DEBUG METHOD CACHE] hit Missing method [DEBUG METHOD CACHE] miss [DEBUG METHOD CACHE] miss [DEBUG METHOD CACHE] hit [DEBUG METHOD CACHE] hit
  46. palkan_tula palkan 105 CACHE = Hash.new { |h, k| h[k]

    = {} } def rd_search_method(klass, mid) return CACHE[klass][mid] if CACHE[klass].key?(mid) # ... loop do # ... # store in cache if found return CACHE[klass][mid] = kl.instance_method(mid) # ... end end ITER #1
  47. palkan_tula palkan 106 user system total real defined 0.003231 0.000350

    0.003581 (0.003594) defined cached 0.002178 0.000146 0.002324 (0.002328) missing 0.064452 0.009929 0.074381 (0.075736) missing cached 0.066092 0.011187 0.077279 (0.079244) ITER #1 still ~30x slower
  48. palkan_tula palkan 107 $ make runruby TESTRUN_SCRIPT=./mcache_test.rb Missing method [DEBUG

    METHOD CACHE] miss [DEBUG METHOD CACHE] miss [DEBUG METHOD CACHE] hit [DEBUG METHOD CACHE] hit cached NULL method entry cached “method_missing”
  49. palkan_tula palkan 108 CACHE = Hash.new { |h, k| h[k]

    = {} } def rd_search_method(klass, mid) return CACHE[klass][mid] if CACHE[klass].key?(mid) # ... loop do # ... # store in cache if found return CACHE[klass][mid] = kl.instance_method(mid) # and if not found too return CACHE[klass][mid] = nil end end ITER #1
  50. palkan_tula palkan 109 user system total real defined 0.003231 0.000350

    0.003581 (0.003594) defined cached 0.002178 0.000146 0.002324 (0.002328) missing 0.064452 0.009929 0.074381 (0.075736) missing cached 0.003820 0.000401 0.004221 (0.004357) ITER #1 <2x slower
  51. palkan_tula palkan 110 #define CALL_METHOD(calling, ci, cc) do { \

    VALUE v = (*(cc) ->call)(ec, GET_CFP(), (calling), (ci), (cc)); \ if (v == Qundef) { \ RESTORE_REGS(); \ NEXT_INSN(); \ } \ else { \ val = v; \ } \ } while (0) METHOD DISPATCH
  52. palkan_tula palkan 111 static void vm_search_method(const struct rb_call_info *ci, struct

    rb_call_cache *cc, VALUE recv) { cc ->me = rb_callable_method_entry( klass, ci ->mid ); cc ->call = vm_call_general; } METHOD DISPATCH
  53. palkan_tula palkan 112 static VALUE vm_call_method_nome(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct

    rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc) { const int stat = ci_missing_reason(ci); if (ci ->mid == idMethodMissing) { vm_raise_method_missing(ec, calling ->argc, argv, calling ->recv, stat); } else { cc ->aux.method_missing_reason = stat; CI_SET_FASTPATH(cc, vm_call_method_missing, 1); return vm_call_method_missing(ec, cfp, calling, ci, cc); } } METHOD DISPATCH
  54. palkan_tula palkan 113 #define CI_SET_FASTPATH(cc, func, enabled) do { \

    if (LIKELY(enabled)) ((cc) ->call = (func)); \ } while (0) METHOD DISPATCH
  55. palkan_tula palkan 115 require_relative "./ruby_dispatch" using RubyDispatch class A def

    foo; true; end end a = A.new p a.rd_send(:foo) # => true A.define_method(:foo) { false } p a.rd_send(:foo) # => true !!! ITER #1
  56. palkan_tula palkan 116 #if OPT_GLOBAL_METHOD_CACHE struct cache_entry *ent; ent =

    GLOBAL_METHOD_CACHE(klass, id); if ( ent ->method_state == GET_GLOBAL_METHOD_STATE() && ent ->class_serial == RCLASS_SERIAL(klass) && ent ->mid == id ) { return ent ->me; #endif METHOD CACHE cache busting
  57. palkan_tula palkan 117 void rb_clear_method_cache_by_class(VALUE klass) { int global =

    klass == rb_cBasicObject || klass == rb_cObject || klass == rb_mKernel; if (global) { INC_GLOBAL_METHOD_STATE(); } else { rb_class_clear_method_cache(klass, Qnil); } } METHOD CACHE
  58. palkan_tula palkan 120 $ ruby -e “p RubyVM.stat” { :global_method_state

    =>138, :global_constant_state =>978, :class_serial =>6494 } vm_stat
  59. palkan_tula palkan 121 Module.prepend(Module.new do %w[append_features prepend_features].each do |mid| module_eval

    <<~SRC def #{mid}(base) CACHE.delete(base); super end SRC end %w[method_added method_removed method_undefined].each do |mid| module_eval <<~SRC def #{mid}(_) CACHE.delete(self); super end SRC end end) ITER #2
  60. palkan_tula palkan 122 require_relative "./ruby_dispatch" using RubyDispatch class A def

    foo; true; end end a = A.new p a.rd_send(:foo) # => true A.define_method(:foo) { false } p a.rd_send(:foo) # => false ITER #2
  61. palkan_tula palkan method_missing 124 Optimized in RubyVM As performant as

    an explicitly defined method… …but not always
  62. palkan_tula palkan 125 class A attr_reader :data def initialize(data) @data

    = data end def data?; data["data"]; end def method_missing(mid) key = mid.to_s.tr('?', '') return unless mid.to_s.match?(/\?$/) && data.key?(key) data[key] end end
  63. palkan_tula palkan 127 class A attr_reader :data def initialize(data) @data

    = data end def data?; data["data"]; end def method_missing(mid) key = mid.to_s.tr('?', '') return unless mid.to_s.match?(/\?$/) && data.key?(key) self.class.define_method(mid) { data[key] } data[key] end end lazy_missing
  64. palkan_tula palkan 128 Comparison: lazy: 994019.1 i/s defined: 992765.2 i/s

    - same-ish missing: 598533.0 i/s - 1.66x slower
  65. palkan_tula palkan 131 def method_missing(method, *args, &block) if @klass.respond_to?(method) self.class

    .delegate_to_scoped_klass(method) scoping { @klass.public_send( method, *args, &block ) } end # ... end RAILS WAY * active_record/relation/delegation.rb
  66. palkan_tula palkan 132 def delegate_to_scoped_klass(method) @delegation_mutex.synchronize do return if method_defined?(method)

    if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) module_eval <<-RUBY, __FILE __, __LINE __ + 1 def #{method}(*args, &block) scoping { @klass. #{method}(*args, &block) } end RUBY else define_method method do |*args, &block| scoping { @klass.public_send(method, *args, &block) } end end end end RAILS WAY * active_record/relation/delegation.rb
  67. palkan_tula palkan 137 Module.prepend(Module.new do def append_features(base) return if base.frozen_itself?

    super end def prepend_features(base) return if base.frozen_itself? super end def method_added(mid) undef_method mid if frozen_itself? end end)
  68. palkan_tula palkan 138 using (Module.new do refine Module do def

    freeze_me! @ __frozen __ = true end def frozen_itself? @ __frozen __ == true && @ __frozen __ignore != true end end end)
  69. palkan_tula palkan 139 module A def foo; end end class

    X def bar; p 'bar'; end freeze_me! end X.prepend A p X.ancestors # => [X, Object, Kernel, BasicObject] X.define_method(:foo) {} X.new.foo # => NoMethodError!!! #NoMonkeyPatching