Slide 1

Slide 1 text

BASIC OBJECTS ERIC ROBERTS | @eroberts

Slide 2

Slide 2 text

PRIMITIVE OBSESSION

Slide 3

Slide 3 text

WHAT IS A PRIMITIVE?

Slide 4

Slide 4 text

In computing, language primitives are the simplest elements available in a programming language. A primitive is the smallest 'unit of processing' available to a programmer of a particular machine, or can be an atomic element of an expression in a language. — Wikipedia

Slide 5

Slide 5 text

IN RUBY

Slide 6

Slide 6 text

boolean = true number = 1 array = [1,2,3,4,5] range = 1..3 hash = { foo: :bar, baz: :qux }

Slide 7

Slide 7 text

OBSESSION

Slide 8

Slide 8 text

"the state of being obsessed with someone or something."

Slide 9

Slide 9 text

REALLY, GOOGLE?

Slide 10

Slide 10 text

"an idea or thought that continually preoccupies or intrudes on a person's mind."

Slide 11

Slide 11 text

PRIMITIVE OBSESSION

Slide 12

Slide 12 text

EVERY ARRAY YOU MAKE

Slide 13

Slide 13 text

Primitive obsession is the practice of using primitives where specialized objects would be more appropriate. For example, using a string to represent a URL or Postal Code.

Slide 14

Slide 14 text

"N1H 7H8"

Slide 15

Slide 15 text

"N1H 7H8".valid? #=> Uh... what?

Slide 16

Slide 16 text

class PostalCode < Struct.new(:code) def valid? # some code that ensures validity end def to_s code end def postal_district code[0] end def forward_sortation_area code[0..3] end def local_delivery_unit code[3..6] end end

Slide 17

Slide 17 text

postal_code = PostalCode.new("N1H 7H8") postal_code.to_s #=> "N1H 7H8" postal_code.postal_district #=> "N" postal_code.forward_sortation_area #=> "N1H" postal_code.local_delivery_unit #=> "7H8"

Slide 18

Slide 18 text

SO, WHAT ABOUT THESE PERCENT THINGS?

Slide 19

Slide 19 text

# Taken from ActionView::Helpers::NumberHelper number_to_percentage(100) #=> 100.000% number_to_percentage("98") #=> 98.000% number_to_percentage(100, precision: 0) #=> 100% number_to_percentage(1000, delimiter: '.', separator: ',') #=> 1.000,000% number_to_percentage(302.24398923423, precision: 5) #=> 302.24399% number_to_percentage(1000, locale: :fr) #=> 1 000,000% number_to_percentage("98a") #=> 98a% number_to_percentage(100, format: "%n %") #=> 100 % number_to_percentage("98a", raise: true) #=> InvalidNumberError

Slide 20

Slide 20 text

THIS HELPS WITH FORMATTING BUT NOT MUCH ELSE.

Slide 21

Slide 21 text

WHAT ELSE COULD YOU DO?

Slide 22

Slide 22 text

# Something like... 50.percent(10) #=> 5

Slide 23

Slide 23 text

MONKEY PATCHING TO THE RESCUE!

Slide 24

Slide 24 text

class Numeric def percent(p) p.to_f / self.to_f * 100.0 end end 100.percent(10) #=> 10

Slide 25

Slide 25 text

OR NOT...

Slide 26

Slide 26 text

SO, WHAT DO WE DO?

Slide 27

Slide 27 text

WE NEED AN OBJECT

Slide 28

Slide 28 text

class Percent def initialize(value) @value = value end end

Slide 29

Slide 29 text

WHAT SHOULD IT DO?

Slide 30

Slide 30 text

percent = Percent.new(50) percent.value #=> 50.0 percent.to_s #=> '50%' percent.to_f #=> 0.5 percent == 50 #=> false percent == 0.5 #=> true

Slide 31

Slide 31 text

attr_reader :value

Slide 32

Slide 32 text

def to_s '%g%%' % value end percent = Percent.new(50) percent.to_s #=> "50%"

Slide 33

Slide 33 text

def to_f value/100 end percent = Percent.new(85) percent.to_f #=> 0

Slide 34

Slide 34 text

WAIT, WHAT?

Slide 35

Slide 35 text

85/100 = 0 85.0/100 = 0.85

Slide 36

Slide 36 text

def initialize(value) @value = value.to_f end percent = Percent.new(85) percent.to_f #=> 0.85

Slide 37

Slide 37 text

def == other (other.class == class && other.value == value) || other == to_f end Percent.new(20) == Percent.new(20) #=> true Percent.new(20) == 0.2 #=> true

Slide 38

Slide 38 text

def eql? other self == other end

Slide 39

Slide 39 text

percent = Percent.new(50) percent.value #=> 50.0 percent.to_s #=> '50%' percent.to_f #=> 0.5 percent == 50 #=> false percent == 0.5 #=> true

Slide 40

Slide 40 text

bigger = Percent.new(90) smaller = Percent.new(10) bigger > smaller #=> true smaller < bigger #=> true

Slide 41

Slide 41 text

def <=> other to_f <=> other.to_f end

Slide 42

Slide 42 text

percent = Percent.new(10) percent + percent #=> Percent.new(20) percent - percent #=> Percent.new(0) percent * percent #=> Percent.new(1) percent / percent #=> 1

Slide 43

Slide 43 text

def + other self.class.new(value + other.value) end percent = Percent.new(10) percent + percent #=> Percent.new(20)

Slide 44

Slide 44 text

def - other self.class.new(value - other.value) end percent = Percent.new(10) percent - percent #=> Percent.new(0)

Slide 45

Slide 45 text

def * other self.class.new(to_f * other.value) end percent = Percent.new(10) percent * percent #=> Percent.new(1)

Slide 46

Slide 46 text

def / other self.class.new(value / other.value) end percent = Percent.new(10) percent / percent #=> 1

Slide 47

Slide 47 text

OK, BUT THAT'S NOT ALL THAT INTERESTING

Slide 48

Slide 48 text

percent = Percent.new(50) percent + 10 #=> Percent.new(60) percent - 10 #=> Percent.new(40) percent * 10 #=> Percent.new(500) percent / 10 #=> Percent.new(5)

Slide 49

Slide 49 text

WE'RE GOING TO FOCUS ON JUST ONE METHOD FOR NOW

Slide 50

Slide 50 text

def * other case other when Percent self.class.new(to_f * other.value) when Numeric self.class.new(value * other) end end percent = Percent.new(50) percent * percent #=> Percent.new(25) percent * 10 #=> Percent.new(500)

Slide 51

Slide 51 text

WHAT ABOUT THE OTHER WAY AROUND?

Slide 52

Slide 52 text

percent = Percent.new(50) 10 + percent #=> 15 10 - percent #=> 5 10 * percent #=> 5 10 / percent #=> 20

Slide 53

Slide 53 text

percent = Percent.new(50) 10 * percent #=> 5

Slide 54

Slide 54 text

NOW THINGS START TO GET INTERESTING

Slide 55

Slide 55 text

#COERCE

Slide 56

Slide 56 text

percent = Percent.new(50) 1 * percent

Slide 57

Slide 57 text

ANY GUESSES AS TO WHAT HAPPENS HERE?

Slide 58

Slide 58 text

1 * percent # percent will receive the coerce message # with the number we are trying to multiply by percent.coerce(1)

Slide 59

Slide 59 text

TypeError: Percent can't be coerced into Fixnum

Slide 60

Slide 60 text

class Percent def coerce other [other, to_f] end end

Slide 61

Slide 61 text

percent = Percent.new(50) 10 * percent percent.coerce(1) #=> [10, 0.5] 10 * 0.5 #=> 5

Slide 62

Slide 62 text

LET'S REVIEW...

Slide 63

Slide 63 text

class Percent [...] def * other case other when Percent self.class.new(to_f * other.value) when Numeric self.class.new(value * other) end end def coerce other [other, to_f] end end

Slide 64

Slide 64 text

WHAT IF "OTHER" IS NOT NUMERIC?

Slide 65

Slide 65 text

percent = Percent.new(50) money = Money.new(100) # What we want to happen percent * money #=> Money.new(50)

Slide 66

Slide 66 text

# What actually happens percent * money #=> nil

Slide 67

Slide 67 text

def * other case other when Percent self.class.new(to_f * other.value) when Numeric self.class.new(value * other) end end

Slide 68

Slide 68 text

def * other case other when Percent self.class.new(to_f * other.value) when Numeric self.class.new(value * other) when Money other * to_f end end

Slide 69

Slide 69 text

#COERCE TO THE RESCUE

Slide 70

Slide 70 text

def * other if other.is_a? Percent self.class.new(to_f * other.value) elsif other.respond_to? :coerce a, b = other.coerce(self) a * b else raise TypeError, "#{other.class} can't be coerced into Percent." end end

Slide 71

Slide 71 text

percent = Percent.new(50) money = Money.new(100) percent * money

Slide 72

Slide 72 text

percent * money money.coerce(percent) # Money receive :coerce with the percent, # and returns the same things in opposite order [money, percent] # Then we try the operation again money * percent percent.coerce(money) # Now, percent receives coerce, with money, # and returns two more things, this time with # the percent changed to float [money, float] # Finally, we can perform this operation # without more coercion money * float Money.new(100) * 0.5 #=> Money.new(50)

Slide 73

Slide 73 text

class Money def coerce(other) [self, other] end end

Slide 74

Slide 74 text

percent * money money.coerce(percent) # Money receive :coerce with the percent, # and returns the same things in opposite order [money, percent] # Then we try the operation again money * percent percent.coerce(money) # Now, percent receives coerce, with money, # and returns two more things, this time with # the percent changed to float [money, float] # Finally, we can perform this operation # without more coercion money * float Money.new(100) * 0.5 #=> Money.new(50)

Slide 75

Slide 75 text

class Percent def coerce other [other, to_f] end end

Slide 76

Slide 76 text

percent * money money.coerce(percent) # Money receive :coerce with the percent, # and returns the same things in opposite order [money, percent] # Then we try the operation again money * percent percent.coerce(money) # Now, percent receives coerce, with money, # and returns two more things, this time with # the percent changed to float [money, float] # Finally, we can perform this operation # without more coercion money * float Money.new(100) * 0.5 #=> Money.new(50)

Slide 77

Slide 77 text

PERCENT KNOWS NOTHING ABOUT MONEY, AND MONEY KNOWS NOTHING ABOUT PERCENT, BUT IT ALL WORKS!

Slide 78

Slide 78 text

MORE COMPLICATED COERCIONS

Slide 79

Slide 79 text

percent = Percent.new(50) 10 + percent #=> 15 10 - percent #=> 5 10 * percent #=> 5 10 / percent #=> 20

Slide 80

Slide 80 text

# Our current coerce method def coerce other [other, to_f] end

Slide 81

Slide 81 text

percent = Percent.new(50) # expected 10 + percent #=> 15 #actual 10 + percent #=> 10.5

Slide 82

Slide 82 text

percent = Percent.new(50) 10 + percent # percent receives coerce, and returns itself # as a float in the second spot percent.coerce(10) #=> [10, 0.5] # Now it adds those two together 10 + 0.5 #=> 10.5

Slide 83

Slide 83 text

# But it works as expected for multiplication! percent = Percent.new(50) 10 * percent percent.coerce(10) #=> [10, 0.5] 10 * 0.5 #=> 5

Slide 84

Slide 84 text

COERCE DOESN'T TELL US WHAT METHOD WAS CALLED, SO WHAT DO WE DO?

Slide 85

Slide 85 text

INVESTIGATE THE CALL STACK!

Slide 86

Slide 86 text

[ "(irb):74:in `*'", "(irb):79:in `irb_binding'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb/workspace.rb:86:in `eval'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb/workspace.rb:86:in `evaluate'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb/context.rb:380:in `evaluate'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb.rb:492:in `block (2 levels) in eval_input'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb.rb:624:in `signal_status'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb.rb:489:in `block in eval_input'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb/ruby-lex.rb:247:in `block (2 levels) in each_top_level_statement'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb/ruby-lex.rb:233:in `loop'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb/ruby-lex.rb:233:in `block in each_top_level_statement'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb/ruby-lex.rb:232:in `catch'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb/ruby-lex.rb:232:in `each_top_level_statement'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb.rb:488:in `eval_input'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb.rb:397:in `block in start'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb.rb:396:in `catch'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/irb.rb:396:in `start'", "/Users/Eric/.rvm/rubies/ruby-2.1.2/bin/irb:11:in `'" ]

Slide 87

Slide 87 text

"(irb):74:in `*'", caller[0].match("`(.+)'")[1].to_sym #=> :*

Slide 88

Slide 88 text

def coerce other method = caller[0].match("`(.+)'")[1].to_sym case other when Numeric case method when :+ [to_f * other, other] else [other, to_f] end else fail TypeError, "#{self.class} can't be coerced into #{other.class}" end end

Slide 89

Slide 89 text

percent = Percent.new(50) # Multiplication 10 * percent percent.coerce(10) #=> [10, 0.5] 10 * 0.5 #=> 5 # Addition 10 + percent percent.coerce(10) #=> [10, 5] 10 + 5 #=> 15

Slide 90

Slide 90 text

DOESN'T REALLY FEEL RIGHT THOUGH, DOES IT?

Slide 91

Slide 91 text

10.plus(percent) percent.coerce(10) #=> [something, something_else] something.plus(something_else)

Slide 92

Slide 92 text

IF IT WALKS LIKE A DUCK AND QUACKS LIKE A DUCK, IT'S PROBABLY RUBY

Slide 93

Slide 93 text

WHAT WE NEED IS AN OBJECT THAT RESPONDS TO #PLUS

Slide 94

Slide 94 text

def coerce other [CoercedPercent.new(self), other] end

Slide 95

Slide 95 text

class CoercedPercent attr_reader :percent def initialize(percent) @percent = percent end def * other other * percent.to_f end def + other other + self * other end end

Slide 96

Slide 96 text

percent = Percent.new(50) # Multiplication 10 * percent percent.coerce(10) #=> [CoercedPercent.new(percent), 10] CoercedPercent.new(percent) * 10 #=> 5 # Addition 10 + percent percent.coerce(10) #=> [CoercedPercent.new(percent), 10] CoercedPercent.new(percent) + 10 #=> 15

Slide 97

Slide 97 text

# We can do the same thing for division and subtraction class CoercedPercent [...] def / other other / percent.to_f end def - other other - self * other end end

Slide 98

Slide 98 text

NOW, LET'S CLEAN SOME THINGS UP

Slide 99

Slide 99 text

BRING ON THE MONKEY PATCHING!

Slide 100

Slide 100 text

module Percentable module Numeric def to_percent Percentable::Percent.new(self) end end end class Numeric include Percentable::Numeric end 10.to_percent #=> Percent.new(10)

Slide 101

Slide 101 text

WHAT ABOUT RAILS?

Slide 102

Slide 102 text

module Percentable module Percentize def percentize *args options = args.pop if args.last.is_a? Hash args.each do |method_name| define_method(method_name) do |args=[]| Percent.new(super(*args) || options[:default]) end end end end end

Slide 103

Slide 103 text

class Thingamajig < ActiveRecord::Base include Percentable::Percentize percentize :taxes, default: 10 end t = Thingamajig.new(taxes: 20) t.taxes #=> Percent.new(20)

Slide 104

Slide 104 text

That's all, folks!

Slide 105

Slide 105 text

FURTHER READING ▸ Percentable by Eric Roberts ▸ Ruby Tapas Episode 206: Coercion by Avdi Grimm ▸ Class Coercion in Ruby by Zach Church ▸ On Obsession, Primitive and Otherwise by Colin Jones ▸ Monkeypatching is Destroying Ruby by Avdi Grimm