Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Let’s Subclass Hash What’s the worst that could happen?

Slide 6

Slide 6 text

My name is Michael Herold. Please tweet me at @mherold or say [email protected].

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

@mherold

Slide 9

Slide 9 text

This talk is about a little gem called … @mherold

Slide 10

Slide 10 text

This talk is about a little gem called … Hashie @mherold

Slide 11

Slide 11 text

“Hashie is a collection of classes and mixins that make hashes more powerful.” @mherold

Slide 12

Slide 12 text

“Hashie is a collection of classes and mixins that make hashes more powerful.” @mherold

Slide 13

Slide 13 text

@mherold

Slide 14

Slide 14 text

@mherold @mherold

Slide 15

Slide 15 text

@mherold @mherold

Slide 16

Slide 16 text

@mherold

Slide 17

Slide 17 text

–Uncle Ben “With great power comes great responsibility.” @mherold

Slide 18

Slide 18 text

–Alexander Pope “To err is human …” @mherold

Slide 19

Slide 19 text

@mherold

Slide 20

Slide 20 text

1. Indifferent Access 2. Mash keys 3. Destructuring a Dash @mherold

Slide 21

Slide 21 text

1. Indifferent Access @mherold

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

class MyHash < Hash end @mherold

Slide 24

Slide 24 text

class MyHash < Hash include Hashie::Extensions::MergeInitializer end @mherold

Slide 25

Slide 25 text

Merge Initializer @mherold hash = MyHash.new( cat: 'meow', dog: { name: 'Rover', sound: 'woof' } )

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

class MyHash < Hash include Hashie::Extensions::MergeInitializer end @mherold

Slide 29

Slide 29 text

class MyHash < Hash include Hashie::Extensions::MergeInitializer include Hashie::Extensions::IndifferentAccess end @mherold

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

class MyHash < Hash include Hashie::Extensions::MergeInitializer include Hashie::Extensions::IndifferentAccess end @mherold

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

@mherold

Slide 37

Slide 37 text

module Hashie::Extensions::IndifferentAccess def merge(*) super.convert! end end @mherold

Slide 38

Slide 38 text

module Hashie::Extensions::IndifferentAccess def merge(*) super.convert! end def convert! # ... end end @mherold

Slide 39

Slide 39 text

What is happening? @mherold

Slide 40

Slide 40 text

hash = MyHash.new( cat: 'meow', 'dog' => { name: 'Mango', sound: 'woof' } ) @mherold

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

We need to go deeper. @mherold

Slide 44

Slide 44 text

Pry + Byebug = @mherold

Slide 45

Slide 45 text

module Hashie::Extensions::IndifferentAccess def merge(*) super.convert! end end @mherold

Slide 46

Slide 46 text

module Hashie::Extensions::IndifferentAccess def merge(*) super.tap { |result| binding.pry }.convert! end end @mherold

Slide 47

Slide 47 text

module Hashie::Extensions::IndifferentAccess def merge(*) super.tap { |result| binding.pry }.convert! end end hash.merge(breed: 'Blue Heeler’) @mherold

Slide 48

Slide 48 text

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(#)> @mherold

Slide 49

Slide 49 text

@mherold

Slide 50

Slide 50 text

self.class #=> Hash @mherold

Slide 51

Slide 51 text

self.class #=> Hash result.class #=> Hash @mherold

Slide 52

Slide 52 text

self.class #=> Hash result.class #=> Hash respond_to?(:convert!) #=> true @mherold

Slide 53

Slide 53 text

self.class #=> Hash result.class #=> Hash respond_to?(:convert!) #=> true result.respond_to?(:convert!) #=> false @mherold

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

module Hashie::Extensions::IndifferentAccess def merge(*) super.convert! end end @mherold

Slide 57

Slide 57 text

module Hashie::Extensions::IndifferentAccess def merge(*) - super.convert! end end @mherold

Slide 58

Slide 58 text

module Hashie::Extensions::IndifferentAccess def merge(*) - super.convert! + result = super + IndifferentAccess.inject!(result) + result.convert! end end @mherold

Slide 59

Slide 59 text

hash = MyHash.new( cat: 'meow', 'dog' => { name: 'Mango', sound: 'woof' } ) @mherold

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Why was this a problem? @mherold

Slide 62

Slide 62 text

Hash has 178 public methods @mherold

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

2. Mash keys @mherold

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

Hashie is almost synonymous with Mash @mherold

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

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 # =>

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

The README used to say “use it for JSON responses” … @mherold

Slide 79

Slide 79 text

… so that’s what people do. @mherold

Slide 80

Slide 80 text

response = HTTP.get(“http://myawesomeapi.com”) @mherold

Slide 81

Slide 81 text

response = HTTP.get(“http://myawesomeapi.com”) json = JSON.parse(response.body) @mherold

Slide 82

Slide 82 text

response = HTTP.get(“http://myawesomeapi.com”) json = JSON.parse(response.body) mash = Hashie::Mash.new(json) @mherold

Slide 83

Slide 83 text

But remember: a Mash is a Hash @mherold

Slide 84

Slide 84 text

Hash has 178 public methods @mherold

Slide 85

Slide 85 text

Would any of these conflict? @mherold class count hash length trust zip

Slide 86

Slide 86 text

mash = Hashie::Mash.new( name: ‘Millenium Biltmore’, zip: ‘90071’ ) @mherold

Slide 87

Slide 87 text

mash = Hashie::Mash.new( name: ‘Millenium Biltmore’, zip: ‘90071’ ) mash.zip #=> [[["name", "Millenium Biltmore"]], [["zip", “90071"]]] @mherold

Slide 88

Slide 88 text

Enumerable#zip @mherold

Slide 89

Slide 89 text

The method is not missing @mherold

Slide 90

Slide 90 text

… so it behaves unexpectedly. @mherold

Slide 91

Slide 91 text

What should we do? @mherold

Slide 92

Slide 92 text

No content

Slide 93

Slide 93 text

Hashie::Extension::MethodAccessWithOverride @mherold class MyMash < Hashie::Mash end

Slide 94

Slide 94 text

Hashie::Extension::MethodAccessWithOverride @mherold class MyMash < Hashie::Mash include Hashie::Extensions::MethodAccessWithOverride end

Slide 95

Slide 95 text

Hashie::Extension::MethodAccessWithOverride @mherold class MyMash < Hashie::Mash include Hashie::Extensions::MethodAccessWithOverride end mash = MyMash.new

Slide 96

Slide 96 text

Hashie::Extension::MethodAccessWithOverride @mherold class MyMash < Hashie::Mash include Hashie::Extensions::MethodAccessWithOverride end mash = MyMash.new mash.awesome = 'sauce'

Slide 97

Slide 97 text

Hashie::Extension::MethodAccessWithOverride @mherold class MyMash < Hashie::Mash include Hashie::Extensions::MethodAccessWithOverride end mash = MyMash.new mash.awesome = 'sauce' mash['awesome'] #=> ‘sauce'

Slide 98

Slide 98 text

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'

Slide 99

Slide 99 text

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'

Slide 100

Slide 100 text

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']]]

Slide 101

Slide 101 text

3. Destructuring a Dash @mherold

Slide 102

Slide 102 text

No content

Slide 103

Slide 103 text

No content

Slide 104

Slide 104 text

No content

Slide 105

Slide 105 text

ruby = { name: ‘Ruby 2.5’, release_date: ‘Christmas’ } @mherold

Slide 106

Slide 106 text

ruby = { name: ‘Ruby 2.5’, release_date: ‘Christmas’ } { **ruby, name: ‘Ruby 2.6’ } #=> {:name=>"Ruby 2.6", :release_date=>”Christmas"} @mherold

Slide 107

Slide 107 text

Dash @mherold class PersonHash < Hashie::Dash end

Slide 108

Slide 108 text

Dash @mherold class PersonHash < Hashie::Dash property :name property :nickname end

Slide 109

Slide 109 text

Dash @mherold class PersonHash < Hashie::Dash property :name property :nickname end PersonHash.new(foo: ‘bar’) #=> NoMethodError: The property 'foo' is not defined

Slide 110

Slide 110 text

@mherold

Slide 111

Slide 111 text

@mherold sam = PersonHash.new(name: ‘Samwise’, nickname: ‘Sam’)

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

@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

Slide 114

Slide 114 text

@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”

Slide 115

Slide 115 text

@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”

Slide 116

Slide 116 text

Why? @mherold

Slide 117

Slide 117 text

What happens when we double-splat? @mherold

Slide 118

Slide 118 text

class Test def to_hash { foo: ‘bar’ } end end @mherold

Slide 119

Slide 119 text

class Test def to_hash { foo: ‘bar’ } end end { **Test.new, baz: ‘quux’ } => {:foo=>"bar", :baz=>”quux"} @mherold

Slide 120

Slide 120 text

No content

Slide 121

Slide 121 text

What happens when we double-splat inside a Hash literal? @mherold

Slide 122

Slide 122 text

@mherold { **sam, height: ‘1.66m’ }

Slide 123

Slide 123 text

@mherold “{ **sam, height: ‘1.66m’ }”

Slide 124

Slide 124 text

@mherold RubyVM::InstructionSequence.compile( “{ **sam, height: ‘1.66m’ }” )

Slide 125

Slide 125 text

@mherold RubyVM::InstructionSequence.compile( “{ **sam, height: ‘1.66m’ }” ).disasm

Slide 126

Slide 126 text

@mherold puts RubyVM::InstructionSequence.compile( “{ **sam, height: ‘1.66m’ }” ).disasm

Slide 127

Slide 127 text

@mherold puts RubyVM::InstructionSequence.compile( “{ **sam, height: ‘1.66m’ }” ).disasm == disasm: #@:1 (1,0)-(1,26)>================= 0000 putspecialobject 1 ( 1)[Li] 0002 putself 0003 opt_send_without_block , 0006 opt_send_without_block , 0009 opt_send_without_block , 0012 putspecialobject 1 0014 swap 0015 putobject :height 0017 putstring "1.66m" 0019 opt_send_without_block , 0022 leave

Slide 128

Slide 128 text

@mherold puts RubyVM::InstructionSequence.compile( “{ **sam, height: ‘1.66m’ }” ).disasm == disasm: #@:1 (1,0)-(1,26)>================= 0000 putspecialobject 1 ( 1)[Li] 0002 putself 0003 opt_send_without_block , 0006 opt_send_without_block , 0009 opt_send_without_block , 0012 putspecialobject 1 0014 swap 0015 putobject :height 0017 putstring "1.66m" 0019 opt_send_without_block , 0022 leave

Slide 129

Slide 129 text

Look for core_hash_merge_kwd in Ruby’s source code @mherold

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

Ruby’s VM casts the value to a hash … @mherold

Slide 133

Slide 133 text

… but only when it isn’t already a Hash. @mherold

Slide 134

Slide 134 text

Recall: a Dash is a Hash. @mherold

Slide 135

Slide 135 text

The VM does not call #to_hash @mherold

Slide 136

Slide 136 text

@mherold { **sam, height: ‘1.66m’ } #=> {:name=>"Samwise", :nickname=>"Sam", :height=>"1.66m"}

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

@mherold

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

The VM does not call #merge @mherold

Slide 141

Slide 141 text

Dash’s property logic exists in Ruby so it isn’t run. @mherold

Slide 142

Slide 142 text

Unfortunately, we can’t “fix” this. @mherold

Slide 143

Slide 143 text

So we wrote it up in the README. @mherold

Slide 144

Slide 144 text

@mherold

Slide 145

Slide 145 text

@mherold

Slide 146

Slide 146 text

1. Indifferent Access 2. Mash keys 3. Destructuring a Dash @mherold

Slide 147

Slide 147 text

class MyHash < Hash @mherold

Slide 148

Slide 148 text

Your interface is suddenly 173 methods (and counting) @mherold

Slide 149

Slide 149 text

Do you think you can catch all the corner cases? @mherold

Slide 150

Slide 150 text

(If so, please contact me - we’d love another co-maintainer! ) @mherold

Slide 151

Slide 151 text

But wait …

Slide 152

Slide 152 text

A wild PSA appears!

Slide 153

Slide 153 text

Hashie::Mash @mherold

Slide 154

Slide 154 text

@mherold

Slide 155

Slide 155 text

No content

Slide 156

Slide 156 text

No content

Slide 157

Slide 157 text

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

Slide 158

Slide 158 text

@mherold

Slide 159

Slide 159 text

You might not need Hashie::Mash @mherold

Slide 160

Slide 160 text

json = JSON.parse(<

Slide 161

Slide 161 text

json = JSON.parse(< # foo="bar"> @mherold

Slide 162

Slide 162 text

json = JSON.parse(< # foo="bar"> parsed.foo #=> "bar" @mherold

Slide 163

Slide 163 text

json = JSON.parse(< # foo="bar"> parsed.foo #=> "bar" parsed['foo'] #=> "bar" @mherold

Slide 164

Slide 164 text

json = JSON.parse(< # foo="bar"> parsed.foo #=> "bar" parsed['foo'] #=> "bar" parsed[:foo] #=> "bar" @mherold

Slide 165

Slide 165 text

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

Slide 166

Slide 166 text

Hashie::Mash @mherold

Slide 167

Slide 167 text

@mherold

Slide 168

Slide 168 text

require ‘json’ require ‘ostruct’ @mherold

Slide 169

Slide 169 text

json = <

Slide 170

Slide 170 text

json = < # @mherold

Slide 171

Slide 171 text

json = < # parsed.foo #=> "bar" @mherold

Slide 172

Slide 172 text

json = < # parsed.foo #=> "bar" parsed['foo'] #=> "bar" @mherold

Slide 173

Slide 173 text

json = < # parsed.foo #=> "bar" parsed['foo'] #=> "bar" parsed[:foo] #=> "bar" @mherold

Slide 174

Slide 174 text

json = < # parsed.foo #=> "bar" parsed['foo'] #=> "bar" parsed[:foo] #=> "bar" parsed.bazes #=> ["baz", “quux"] @mherold

Slide 175

Slide 175 text

@mherold

Slide 176

Slide 176 text

My name is Michael Herold. Please tweet me at @mherold or say [email protected].

Slide 177

Slide 177 text

No content