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

Let's Subclass Hash - What's the Worst That Could Happen?

Michael Herold
November 14, 2018

Let's Subclass Hash - What's the Worst That Could Happen?

Have you ever been tempted to subclass a core class like Hash or String? Or have you read blog posts about why you shouldn't do that, but been left confused as to the specifics? As a maintainer of Hashie, a gem that commits this exact sin, I'm here to tell you why you want to reach for other tools instead of the subclass.

In this talk, you'll hear stories from the trenches about what can go wrong when you subclass core classes. We'll dig into Ruby internals and you will leave with a few new tools for tracking down seemingly inexplicable performance issues and bugs in your applications.

See the accompanying blog post at michaeljherold.com.

Michael Herold

November 14, 2018
Tweet

More Decks by Michael Herold

Other Decks in Technology

Transcript

  1. Merge Initializer @mherold hash = MyHash.new( cat: 'meow', dog: {

    name: 'Rover', sound: 'woof' } ) hash[:cat] #=> "meow"
  2. Merge Initializer @mherold hash = MyHash.new( cat: 'meow', dog: {

    name: 'Rover', sound: 'woof' } ) hash[:cat] #=> "meow" hash[:dog] #=> {:name=>"Rover", :sound=>"woof"}
  3. Indifferent Access @mherold hash = MyHash.new( cat: 'meow', 'dog' =>

    { name: 'Rover', sound: 'woof' } ) hash['cat'] == hash[:cat] #=> true
  4. Indifferent Access @mherold hash = MyHash.new( cat: 'meow', 'dog' =>

    { name: 'Rover', sound: 'woof' } ) hash['cat'] == hash[:cat] #=> true hash['dog'] == hash[:dog] #=> true
  5. class MyHash < Hash include Hashie::Extensions::MergeInitializer include Hashie::Extensions::IndifferentAccess end hash

    = MyHash.new( cat: 'meow', 'dog' => { name: 'Mango', sound: 'woof' } ) @mherold
  6. class MyHash < Hash include Hashie::Extensions::MergeInitializer include Hashie::Extensions::IndifferentAccess end hash

    = MyHash.new( cat: 'meow', 'dog' => { name: 'Mango', sound: 'woof' } ) new_dog = hash[:dog].merge(breed: 'Blue Heeler') #=> NoMethodError: undefined method `convert!' @mherold
  7. hash = MyHash.new( cat: 'meow', 'dog' => { name: 'Mango',

    sound: 'woof' } ) hash.respond_to?(:convert!) #=> true @mherold
  8. hash = MyHash.new( cat: 'meow', 'dog' => { name: 'Mango',

    sound: 'woof' } ) hash.respond_to?(:convert!) #=> true hash[:dog].respond_to?(:convert!) #=> true @mherold
  9. module Hashie::Extensions::IndifferentAccess def merge(*) super.tap { |result| binding.pry }.convert! end

    end hash.merge(breed: 'Blue Heeler’) 134: def merge(*args) => 135: super.tap { |result| binding.pry }.convert! 136: end [1] pry(#<Pry::Config>)> @mherold
  10. self.class #=> Hash result.class #=> Hash respond_to?(:convert!) #=> true result.respond_to?(:convert!)

    #=> false singleton_class.ancestors #=> […, Hashie::Extensions::IndifferentAccess, …] @mherold
  11. self.class #=> Hash result.class #=> Hash respond_to?(:convert!) #=> true result.respond_to?(:convert!)

    #=> false singleton_class.ancestors #=> […, Hashie::Extensions::IndifferentAccess, …] result.singleton_class.ancestors #=> No indifferent access @mherold
  12. module Hashie::Extensions::IndifferentAccess def merge(*) - super.convert! + result = super

    + IndifferentAccess.inject!(result) + result.convert! end end @mherold
  13. hash = MyHash.new( cat: 'meow', 'dog' => { name: 'Mango',

    sound: 'woof' } ) new_dog = hash[:dog].merge(breed: 'Blue Heeler') #=> {"name"=>"Rover", "sound"=>"woof", "breed"=>"Blue Heeler"} @mherold
  14. Mash @mherold mash = Hashie::Mash.new mash.name? # => false mash.name

    # => nil mash.name = "My Mash” mash.name # => "My Mash" mash.name? # => true mash.inspect # => <Hashie::Mash name="My Mash">
  15. Mash @mherold def method_missing(method_name, *args, &blk) return self.[](method_name, &blk) if

    key?(method_name) name, suffix = method_name_and_suffix(method_name) end
  16. Mash @mherold def method_missing(method_name, *args, &blk) return self.[](method_name, &blk) if

    key?(method_name) name, suffix = method_name_and_suffix(method_name) case suffix when ‘='.freeze then assign_property(name, args.first) end end
  17. Mash @mherold def method_missing(method_name, *args, &blk) return self.[](method_name, &blk) if

    key?(method_name) name, suffix = method_name_and_suffix(method_name) case suffix when ‘='.freeze then assign_property(name, args.first) when ‘?'.freeze then !!self[name] end end
  18. Mash @mherold def method_missing(method_name, *args, &blk) return self.[](method_name, &blk) if

    key?(method_name) name, suffix = method_name_and_suffix(method_name) case suffix when ‘='.freeze then assign_property(name, args.first) when ‘?'.freeze then !!self[name] when ‘!'.freeze then initializing_reader(name) end end
  19. Mash @mherold def method_missing(method_name, *args, &blk) return self.[](method_name, &blk) if

    key?(method_name) name, suffix = method_name_and_suffix(method_name) case suffix when ‘='.freeze then assign_property(name, args.first) when ‘?'.freeze then !!self[name] when ‘!'.freeze then initializing_reader(name) when ‘_'.freeze then underbang_reader(name) end end
  20. Mash @mherold def method_missing(method_name, *args, &blk) return self.[](method_name, &blk) if

    key?(method_name) name, suffix = method_name_and_suffix(method_name) case suffix when ‘='.freeze then assign_property(name, args.first) when ‘?'.freeze then !!self[name] when ‘!'.freeze then initializing_reader(name) when ‘_'.freeze then underbang_reader(name) else self[method_name] end end
  21. mash = Hashie::Mash.new( name: ‘Millenium Biltmore’, zip: ‘90071’ ) mash.zip

    #=> [[["name", "Millenium Biltmore"]], [["zip", “90071"]]] @mherold
  22. Hashie::Extension::MethodAccessWithOverride @mherold class MyMash < Hashie::Mash include Hashie::Extensions::MethodAccessWithOverride end mash

    = MyMash.new mash.awesome = 'sauce' mash['awesome'] #=> 'sauce' mash.zip = 'a-dee-doo-dah' mash.zip #=> 'a-dee-doo-dah'
  23. Hashie::Extension::MethodAccessWithOverride @mherold class MyMash < Hashie::Mash include Hashie::Extensions::MethodAccessWithOverride end mash

    = MyMash.new mash.awesome = 'sauce' mash['awesome'] #=> 'sauce' mash.zip = 'a-dee-doo-dah' mash.zip #=> 'a-dee-doo-dah' mash.__zip #=> [[['awesome', 'sauce'], ['zip', 'a-dee-doo-dah']]]
  24. ruby = { name: ‘Ruby 2.5’, release_date: ‘Christmas’ } {

    **ruby, name: ‘Ruby 2.6’ } #=> {:name=>"Ruby 2.6", :release_date=>”Christmas"} @mherold
  25. Dash @mherold class PersonHash < Hashie::Dash property :name property :nickname

    end PersonHash.new(foo: ‘bar’) #=> NoMethodError: The property 'foo' is not defined
  26. @mherold sam = PersonHash.new(name: ‘Samwise’, nickname: ‘Sam’) result = {

    **sam, height: ‘1.66m’ } #=> {:name=>"Samwise", :nickname=>"Sam", :height=>"1.66m"}
  27. @mherold sam = PersonHash.new(name: ‘Samwise’, nickname: ‘Sam’) result = {

    **sam, height: ‘1.66m’ } #=> {:name=>"Samwise", :nickname=>"Sam", :height=>"1.66m"} result[:height] #=> NoMethodError: The property 'height' is not defined
  28. @mherold sam = PersonHash.new(name: ‘Samwise’, nickname: ‘Sam’) result = {

    **sam, height: ‘1.66m’ } #=> {:name=>"Samwise", :nickname=>"Sam", :height=>"1.66m"} result[:height] #=> NoMethodError: The property 'height' is not defined { height: ‘1.66m’, **sam }[:height] #=> “1.66m”
  29. @mherold sam = PersonHash.new(name: ‘Samwise’, nickname: ‘Sam’) result = {

    **sam, height: ‘1.66m’ } #=> {:name=>"Samwise", :nickname=>"Sam", :height=>"1.66m"} result[:height] #=> NoMethodError: The property 'height' is not defined { height: ‘1.66m’, **sam }[:height] #=> “1.66m” { **sam.to_h, height: ‘1.66m’ }[:height] #=> “1.66m”
  30. class Test def to_hash { foo: ‘bar’ } end end

    { **Test.new, baz: ‘quux’ } => {:foo=>"bar", :baz=>”quux"} @mherold
  31. @mherold puts RubyVM::InstructionSequence.compile( “{ **sam, height: ‘1.66m’ }” ).disasm ==

    disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,26)>================= 0000 putspecialobject 1 ( 1)[Li] 0002 putself 0003 opt_send_without_block <callinfo!mid:sam, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache> 0006 opt_send_without_block <callinfo!mid:core#hash_merge_kwd, argc:1, ARGS_SIMPLE>, <callcache> 0009 opt_send_without_block <callinfo!mid:dup, argc:0, ARGS_SIMPLE>, <callcache> 0012 putspecialobject 1 0014 swap 0015 putobject :height 0017 putstring "1.66m" 0019 opt_send_without_block <callinfo!mid:core#hash_merge_ptr, argc:3, ARGS_SIMPLE>, <callcache> 0022 leave
  32. @mherold puts RubyVM::InstructionSequence.compile( “{ **sam, height: ‘1.66m’ }” ).disasm ==

    disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,26)>================= 0000 putspecialobject 1 ( 1)[Li] 0002 putself 0003 opt_send_without_block <callinfo!mid:sam, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache> 0006 opt_send_without_block <callinfo!mid:core#hash_merge_kwd, argc:1, ARGS_SIMPLE>, <callcache> 0009 opt_send_without_block <callinfo!mid:dup, argc:0, ARGS_SIMPLE>, <callcache> 0012 putspecialobject 1 0014 swap 0015 putobject :height 0017 putstring "1.66m" 0019 opt_send_without_block <callinfo!mid:core#hash_merge_ptr, argc:3, ARGS_SIMPLE>, <callcache> 0022 leave
  33. @mherold static VALUE core_hash_merge_kwd(int argc, VALUE *argv) { VALUE hash,

    kw; rb_check_arity(argc, 1, 2); hash = argv[0]; kw = rb_to_hash_type(argv[argc-1]); if (argc < 2) hash = kw; rb_hash_foreach(kw, argc < 2 ? kwcheck_i : kwmerge_i, hash); return hash; }
  34. @mherold static VALUE core_hash_merge_kwd(int argc, VALUE *argv) { VALUE hash,

    kw; rb_check_arity(argc, 1, 2); hash = argv[0]; kw = rb_to_hash_type(argv[argc-1]); if (argc < 2) hash = kw; rb_hash_foreach(kw, argc < 2 ? kwcheck_i : kwmerge_i, hash); return hash; }
  35. @mherold { **sam, height: ‘1.66m’ } #=> {:name=>"Samwise", :nickname=>"Sam", :height=>"1.66m"}

    sam.merge(height: ‘1.66m’) #=> NoMethodError: The property 'height' is not defined
  36. @mherold static VALUE core_hash_merge_kwd(int argc, VALUE *argv) { VALUE hash,

    kw; rb_check_arity(argc, 1, 2); hash = argv[0]; kw = rb_to_hash_type(argv[argc-1]); if (argc < 2) hash = kw; rb_hash_foreach(kw, argc < 2 ? kwcheck_i : kwmerge_i, hash); return hash; }
  37. Gem Name Total Downloads Rank omniauth 199 inspec 262 elasticsearch-api

    264 elasticsearch-transport 265 restforce 567 chef-zero 716 elasticsearch-model 782 ridley 890 zendesk_api 911 Data from the 2018-11-12 RubyGems.org data dump Queries can be found at https://michaeljherold.com/rubyconf2018 @mherold
  38. json = JSON.parse(<<JSON) { "foo": "bar", "bazes": [ "baz", "quux"

    ] } JSON parsed = Hashie::Mash.new(json, object_class: OpenStruct) #=> #<Hashie::Mash bazes=["baz", "quux"]> foo="bar"> @mherold
  39. json = JSON.parse(<<JSON) { "foo": "bar", "bazes": [ "baz", "quux"

    ] } JSON parsed = Hashie::Mash.new(json, object_class: OpenStruct) #=> #<Hashie::Mash bazes=["baz", "quux"]> foo="bar"> parsed.foo #=> "bar" @mherold
  40. json = JSON.parse(<<JSON) { "foo": "bar", "bazes": [ "baz", "quux"

    ] } JSON parsed = Hashie::Mash.new(json, object_class: OpenStruct) #=> #<Hashie::Mash bazes=["baz", "quux"]> foo="bar"> parsed.foo #=> "bar" parsed['foo'] #=> "bar" @mherold
  41. json = JSON.parse(<<JSON) { "foo": "bar", "bazes": [ "baz", "quux"

    ] } JSON parsed = Hashie::Mash.new(json, object_class: OpenStruct) #=> #<Hashie::Mash bazes=["baz", "quux"]> foo="bar"> parsed.foo #=> "bar" parsed['foo'] #=> "bar" parsed[:foo] #=> "bar" @mherold
  42. json = JSON.parse(<<JSON) { "foo": "bar", "bazes": [ "baz", "quux"

    ] } JSON parsed = Hashie::Mash.new(json, object_class: OpenStruct) #=> #<Hashie::Mash bazes=["baz", "quux"]> foo="bar"> parsed.foo #=> "bar" parsed['foo'] #=> "bar" parsed[:foo] #=> "bar" parsed.bazes #=> ["baz", “quux"] @mherold
  43. json = <<JSON { "foo": "bar", "bazes": [ "baz", "quux"

    ] } JSON parsed = JSON.parse(json, object_class: OpenStruct) #=> #<OpenStruct foo="bar", bazes=["baz", "quux"]> @mherold
  44. json = <<JSON { "foo": "bar", "bazes": [ "baz", "quux"

    ] } JSON parsed = JSON.parse(json, object_class: OpenStruct) #=> #<OpenStruct foo="bar", bazes=["baz", "quux"]> parsed.foo #=> "bar" @mherold
  45. json = <<JSON { "foo": "bar", "bazes": [ "baz", "quux"

    ] } JSON parsed = JSON.parse(json, object_class: OpenStruct) #=> #<OpenStruct foo="bar", bazes=["baz", "quux"]> parsed.foo #=> "bar" parsed['foo'] #=> "bar" @mherold
  46. json = <<JSON { "foo": "bar", "bazes": [ "baz", "quux"

    ] } JSON parsed = JSON.parse(json, object_class: OpenStruct) #=> #<OpenStruct foo="bar", bazes=["baz", "quux"]> parsed.foo #=> "bar" parsed['foo'] #=> "bar" parsed[:foo] #=> "bar" @mherold
  47. json = <<JSON { "foo": "bar", "bazes": [ "baz", "quux"

    ] } JSON parsed = JSON.parse(json, object_class: OpenStruct) #=> #<OpenStruct foo="bar", bazes=["baz", "quux"]> parsed.foo #=> "bar" parsed['foo'] #=> "bar" parsed[:foo] #=> "bar" parsed.bazes #=> ["baz", “quux"] @mherold