I’ve just started reading Confident Ruby by Avdi Grim and so far I love it.
Sometimes I write code that is so nervous it is bordering on needy:
def cool_action(input)
# Hey input just checking that you are not nil - I definitely don’t want you to be nil
unless input.nil?
# Oh and while I’m asking, are you a string. That would be good, in fact it would be catastrophic if you weren’t
# I mean it would be ok for you to look like something other than a string but for now could you just pretend you were a string
if input.is_a? String
# Ok, now we have that out of the way, lets do the cool_action
puts "That's so cool " + input
end
end
end
Avdi is giving me the confidence to throw away many of the reassurance checks that litter my code. In his book he refers to this as ‘Defensive Programming’ and insists it has a place but should be restricted to the borders of your code.
As an example, an API might be considered to be an obvious border where objects passing through them can be checked.
The public methods of these gatekeeper objects are a natural site for defensive code. Once objects pass through this border guard, they can be implicitly trusted to be good neighbours.
One strategy he uses to avoid writing code defensively is to ‘Coerce objects into the roles we need them to play’ and this is where type converters come into play.
You are probably yelling at me to change my cool method to:
def cool_action(input)
puts "That's so cool " + input.to_s
end
And that is what I am talking about - type conversion!
You might also have yelled “What about string interpolation” and again you’d be right but don’t jump ahead!
Implicit vs Explicit Ruby Type Conversion
Explicit | Implicit |
---|---|
to_s | to_str |
to_i | to_int |
to_a | to_ary |
to_h | to_hash |
to_sym | |
to_proc | |
to_enum |
Merriam-Webster informs me that:
Explicit denotes being very clear and complete without vagueness, implication, or ambiguity
Implicit, on the other hand, denotes that something is understood although not clearly or directly expressed or conveyed
With Ruby type conversions, explicit types represent conversion from classes which are mostly unrelated to the target class. Eg converting Time to a string. These should be implemented if the class can somehow be converted to the target class (most classes in Ruby do implement a #to_s method so that can view a textual representation of an object). Similarly if you are writing a method and expect the input data to have a means of being converted to the target type, you should call this method on it.
Implicit type conversions represent conversion between more closely related classes. You should implement these for a class if it could be considered to be “kind of” a target class. It can be problematic to declare a implicit conversion method - see the example on Zverok’s excellent post.
The link back to the dictionary definition of these two terms, relates to the way that Ruby and core classes interact with these methods. Many core methods use implicit conversion of data:
"string" + other
will call#to_str
on other,[1,2,3] + other
will call#to_ary
and so on (and will raiseTypeError: no implicit conversion
if object does not respond to this method);"string" * other
and[1,2,3] * other
will call#to_int
on other;a, b, c = *other
will callother#to_ary
(and, therefore, can be used to “unpack” custom collection objects but also can cause unintended behavior, if other implements to_ary but was not thought as a collection);some_method(*other)
— same as above, usesother#to_ary
;some_method(**other)
— usesother#to_hash
.
Bullet list stolen from Zverok’s implicit explicit post]
Conversely, Ruby will almost never, automatically use an explicit conversion method. You need to explicitly send messages like #to_i or #to_s.
The exception to the rule is string interpolation where we want to be able to show non-stringy objects as a string. In this case Ruby will call #to_s on the object in question.
Implementing Explicit Type Conversions
I recently worked with coins that have both an integer value and a textual face value.
Coin = Struct.new(:face, :pence)
two_pounds = Coin.new('£2', 200)
=> #<struct Coin face="£2", pence=200>
I wanted to be able to pass these coins around showing them individually in a text string as well as doing maths on them. To return something like: “You have entered a £2 coin and a 50p coin. That is a total of 250 pence.”
I had access to a two-number adder method
def add_two(val1, val2)
val1 + val2
end
but to use it with my coins I had to remember to pass in two_pounds.pence
and fifty_pence.pence
and similarly with my string concatenation it was two_pounds.face
and fifty_pence.face
. Super UGLY.
If I’d implemented some explicit methods in my Coin structure and I’d been a little less dictatorial in my add_two
method, I could have just passed my coin objects in as they stood.
Coin = Struct.new(:face, :pence) do
def to_int
pence
end
def to_i
to_int
end
def to_str
face
end
def to_s
to_str
end
end
two_pounds = Coin.new('£2', 200)
fifty_pence = Coin.new('50p', 50)
puts("You have entered a #{two_pounds} and #{fifty_pence}. That is a total of #{add_two(two_pounds, fifty_pence)} pence.")
Because I have defined both the implicit and explicit methods for my coin structure. This also works:
puts("You have entered a " + two_pounds + "and " + fifty_pence + ". That is a total of " + add_two(two_pounds, fifty_pence) + " pence.")
Duck Typing
A method may perform actions on integers but does it really need the input to be an Integer? Perhaps it is good enough for it to just quack like an integer.
If it quacks like a duck, it’s a duck
So if I’m using your calculator class that has a method to add two numbers together, it’s fine that this method works with integer inputs but why restrict it to only inputs whose class is Integer. I want you to take my two coins, and take them for a spin - see if they quack like integers.
Mind you, sometimes it might be really, really important that the input to your method is an Integer. In those cases you don’t want to be accepting an ‘almost Integer’ where nil could be coerced to zero. If your method determines launch velocity of a space shuttle you might want to use an implicit converter (to_int) which should only ever be defined for Integer and Integer-like objects, all others would raise a no method error (and hopefully be detected in your pre-launch tests).
Comments