Slide 1

Slide 1 text

palkan_tula palkan METACREATIVITY Vladimir Dementyev Ruby. Creativity. Metaprogramming

Slide 2

Slide 2 text

palkan_tula palkan 2 Vladimir Dementyev

Slide 3

Slide 3 text

palkan_tula palkan 3 @palkan @palkan_tula

Slide 4

Slide 4 text

palkan_tula palkan 4

Slide 5

Slide 5 text

palkan_tula palkan 5

Slide 6

Slide 6 text

palkan_tula palkan https://evilmartians.com 6

Slide 7

Slide 7 text

palkan_tula palkan https://evilmartians.com 7

Slide 8

Slide 8 text

palkan_tula palkan https://evilmartians.com 8

Slide 9

Slide 9 text

palkan_tula palkan THE TALK 9

Slide 10

Slide 10 text

palkan_tula palkan RUBY: THE GOOD PARTS 10

Slide 11

Slide 11 text

palkan_tula palkan #BalkanRuby 11

Slide 12

Slide 12 text

palkan_tula palkan COMMUNITY 12 SOFT TALK DETECTED

Slide 13

Slide 13 text

palkan_tula palkan #RubyKaigi 13

Slide 14

Slide 14 text

palkan_tula palkan TECH TALK 14

Slide 15

Slide 15 text

palkan_tula palkan LANGUAGE FEATURES 15

Slide 16

Slide 16 text

palkan_tula palkan META \ PROGRAMMING 16

Slide 17

Slide 17 text

palkan_tula palkan INTERNALS 17

Slide 18

Slide 18 text

palkan_tula palkan Part 1 CREATIVITY

Slide 19

Slide 19 text

palkan_tula palkan WROC_LOVE.RB 2018 19

Slide 20

Slide 20 text

palkan_tula palkan 20 Writing code as close to natural language as possible

Slide 21

Slide 21 text

palkan_tula palkan 21 def change add_column :profiles, :joined_at, :date add_index :profiles, :joined_at end

Slide 22

Slide 22 text

palkan_tula palkan 22 Designing a good DSL is a creative process

Slide 23

Slide 23 text

palkan_tula palkan DSL 23 Brings simplicity & readability Hides complexity & internals

Slide 24

Slide 24 text

palkan_tula palkan GOOD DSL 24 Brings simplicity & readability Hides complexity & internals Makes developers happy (through flexibility, testability, whatever-ability)

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

palkan_tula palkan 26 class User < ApplicationRecord belongs_to :account has_many :posts end

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

palkan_tula palkan 28 Write code the same way you think

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

palkan_tula palkan CREATIVITY 30 field :user, Types ::User, null: true batch_load :user

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

palkan_tula palkan 33 Make your craziest ideas true

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

palkan_tula palkan 37 …and some real art

Slide 38

Slide 38 text

palkan_tula palkan LIVE MUSIC *RubyConf 2017: Live Coding Music with Sonic Pi by Sam Aaron 38

Slide 39

Slide 39 text

palkan_tula palkan ruby_for_artists 39 * https://github.com/halfbyte/ruby_for_artists

Slide 40

Slide 40 text

palkan_tula palkan 40 These things are possible due to metaprogramming

Slide 41

Slide 41 text

palkan_tula palkan Part 2 META Define it

Slide 42

Slide 42 text

palkan_tula palkan 42 Metaprogramming literally means to write a program that can, in turn, write another program

Slide 43

Slide 43 text

palkan_tula palkan 43 Metaprogramming means to write a program that can, in turn, modify another program behaviour

Slide 44

Slide 44 text

palkan_tula palkan METAPROGRAMMING 44 Code generation Code removal Syntax extension Introspection

Slide 45

Slide 45 text

palkan_tula palkan INTROSPECTION 45 Retrieve information about a program being executed Not a metaprogramming itself but heavily used for

Slide 46

Slide 46 text

palkan_tula palkan METARUBY 46 Open classes Dynamic dispatch Reflection API Module/method hooks

Slide 47

Slide 47 text

palkan_tula palkan METARUBY 47 Binding Refinements TracePoint #hardcore

Slide 48

Slide 48 text

palkan_tula palkan #RubyKaigi https://rubykaigi.org/2018/presentations/joker1007.html 48

Slide 49

Slide 49 text

palkan_tula palkan HOW DO THINGS WORK? 49

Slide 50

Slide 50 text

palkan_tula palkan Part 3 MISSING The tale of method_missing

Slide 51

Slide 51 text

palkan_tula palkan 51 Everything is an object DYNAMIC DISPATCH

Slide 52

Slide 52 text

palkan_tula palkan 52 Almost everything is an object DYNAMIC DISPATCH

Slide 53

Slide 53 text

palkan_tula palkan OBJECT MODEL 53 https://www.youtube.com/watch?v=IbyPipTH1r0

Slide 54

Slide 54 text

palkan_tula palkan 54 Everything is a BasicObject DYNAMIC DISPATCH

Slide 55

Slide 55 text

palkan_tula palkan 55 To call a method is to “send” a message from one object to another DYNAMIC DISPATCH

Slide 56

Slide 56 text

palkan_tula palkan 56 Handle unknown messages (or undefined methods) method_missing

Slide 57

Slide 57 text

palkan_tula palkan 57 class BasicObject private def method_missing(*) # C code to raise an exception end end

Slide 58

Slide 58 text

palkan_tula palkan 58 method_missing is called iff method is not defined TRUTH OR LIE

Slide 59

Slide 59 text

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”

Slide 60

Slide 60 text

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); } }

Slide 61

Slide 61 text

palkan_tula palkan method_missing 61 Hashie ActiveRecord dynamic matchers Many other Proxy pattern implementations

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

palkan_tula palkan 64 undef_method vs. remove_method

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

palkan_tula palkan 67 method_missing is slow TRUTH OR LIE

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

palkan_tula palkan HOW RUBY EXECUTES CODE? 72

Slide 73

Slide 73 text

palkan_tula palkan 73 .rb Tokenize Parse Compile ISeq

Slide 74

Slide 74 text

palkan_tula palkan 74 Compiled sequence of instructions for the RubyVM ISeq

Slide 75

Slide 75 text

palkan_tula palkan 75 def hi puts "hello, world" end puts RubyVM ::InstructionSequence .disasm(method(:hi)) ISeq

Slide 76

Slide 76 text

palkan_tula palkan 76 $ ruby --dump=i -e "1 + 2” == disasm: #@-e:1 (1,0)-(1,5)>============== 0000 putobject_OP_INT2FIX_O_1_C_ ( 1)[Li] 0001 putobject 2 0003 opt_plus 0006 leave ISeq

Slide 77

Slide 77 text

palkan_tula palkan INVESTIGATION 77 Generate ISeq Lookup instructions definitions in / insns.def Follow CRuby code :)

Slide 78

Slide 78 text

palkan_tula palkan 78 $ ruby --dump=i -e ' class A def foo; end end A.new.foo' == disasm: #@-e:1 (1,0)-(6,9)>============== … 0003 defineclass :A, , 0 … 0015 opt_send_without_block , 0018 opt_send_without_block , 0021 leave METHOD DISPATCH

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

palkan_tula palkan 83

Slide 84

Slide 84 text

palkan_tula palkan 84 Let’s re-write method dispatch in Ruby!* * only for demonstration purposes

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

palkan_tula palkan 89 module RubyDispatch refine BasicObject do def rd_search_method(klass, mid) # ??? end end end ITER #0

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

palkan_tula palkan Shawarma#eatable? 91 Shawarma Food Eatable included modules parent #eatable?

Slide 92

Slide 92 text

palkan_tula palkan 92 module Inedible def eatable? false end end Shawarma.prepend Inedible LOOKUP

Slide 93

Slide 93 text

palkan_tula palkan Shawarma#eatable? 93 Shawarma Food Eatable included modules parent #eatable? Inedible #eatable?

Slide 94

Slide 94 text

palkan_tula palkan 94 Shawarma.ancestors # [ # Inedible, # Shawarma, # Food, # Eatable, # Object, # Kernel, # BasicObject # ] LOOKUP

Slide 95

Slide 95 text

palkan_tula palkan include/prepend 95 Do no add methods to the target class Only affect lookup hierarchy

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

palkan_tula palkan 100 $ ruby --dump=i -e ' class A def foo; end end A.new.foo' == disasm: #@-e:1 (1,0)-(6,9)>============== … 0003 defineclass :A, , 0 … 0015 opt_send_without_block , 0018 opt_send_without_block , 0021 leave METHOD DISPATCH

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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”

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

palkan_tula palkan 113 #define CI_SET_FASTPATH(cc, func, enabled) do { \ if (LIKELY(enabled)) ((cc) ->call = (func)); \ } while (0) METHOD DISPATCH

Slide 114

Slide 114 text

palkan_tula palkan WHAT ABOUT CACHE INVALIDATION? 114

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

palkan_tula palkan 118 rb_class_clear_method_cache(VALUE klass, VALUE arg) { RCLASS_SERIAL(klass) = rb_next_class_serial(); # ... } METHOD CACHE

Slide 119

Slide 119 text

palkan_tula palkan rb_clear_method_cache_by_class 119 Method is added/removed Module is prepended/included with overlapping methods Refinement is created

Slide 120

Slide 120 text

palkan_tula palkan 120 $ ruby -e “p RubyVM.stat” { :global_method_state =>138, :global_constant_state =>978, :class_serial =>6494 } vm_stat

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

palkan_tula palkan 123 http://bit.ly/ruby-dispatch RubyDispatch

Slide 124

Slide 124 text

palkan_tula palkan method_missing 124 Optimized in RubyVM As performant as an explicitly defined method… …but not always

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

palkan_tula palkan 126 Comparison: defined: 997170.8 i/s missing: 658201.2 i/s - 1.51x slower

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

palkan_tula palkan 129 define_method vs. class_eval

Slide 130

Slide 130 text

palkan_tula palkan 130 * https://tenderlovemaking.com/2013/03/03/dynamic_method_definitions.html

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

palkan_tula palkan 133 binding.pry

Slide 134

Slide 134 text

palkan_tula palkan In place of outro FREEDOM & PATCHING

Slide 135

Slide 135 text

palkan_tula palkan 135

Slide 136

Slide 136 text

palkan_tula palkan 136 Use monkey-patching to prevent monkey-patching

Slide 137

Slide 137 text

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)

Slide 138

Slide 138 text

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)

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

palkan_tula palkan THANKS? QUESTIONS! Vladimir Dementyev evilmartians.com/blog @evilmartians