-
23
MAY
2015Rich Methods
Some APIs provide collections of dirt simple methods that just do one little thing.
This approach in less common in Ruby though, especially in the core and standard library of the language itself. Ruby often gives us rich methods with lots of switches we can toggle and half hidden behaviors.
Let's look at some examples of what I am talking about.
Get a Line at a Time
I suspect most Rubyists have used
gets()
to read lines of input from some kind ofIO
. Here's the basic usage:>> require "stringio" => true >> f = StringIO.new(<<END_STR) <xml> <tags>Content</tags> </xml> END_STR => #<StringIO:0x007fd5a264fa08> >> f.gets => "<xml>\n" >> f.gets => " <tags>Content</tags>\n"
I didn't want to mess with external files for these trivial examples, so I just loaded
StringIO
from the standard library. It allows us to wrap a simpleString
(defined in this example using the heredoc syntax) in theIO
interface. In other words, I'm callinggets()
here for aString
just as I could with aFile
or$stdin
. -
24
OCT
2014The Three Tick Sort
Yesterday I showed a newer programmer some code like
scores.sort_by(&:reverse)
. This provoked a comment about how they where going to look upsort_by()
later to figure out what magic is involved here. It made me sad to realize how many cool tricks they weren't going to see in that bit of documentation.Allow me to enumerate those tricks for you, but first let's flesh out an example. Consider this code:
scores = { fifteen: 2, five_card_run: 5, five_card_flush: 5, four_card_run: 4, four_card_flush: 4, his_nobs: 1, pair: 2, three_card_run: 3, } scores.sort_by(&:reverse).each do |name, score| puts "Score #{score} for #{name}." end # >> Score 1 for his_nobs. # >> Score 2 for fifteen. # >> Score 2 for pair. # >> Score 3 for three_card_run. # >> Score 4 for four_card_flush. # >> Score 4 for four_card_run. # >> Score 5 for five_card_flush. # >> Score 5 for five_card_run.
In this case, the magic method call (
scores.sort_by(&:reverse)
) has reordered a list of Cribbage hands first by point value and then alphabetically ("ASCIIabetically" in truth). How this happens is a pretty interesting journey though. -
10
OCT
2008All About Struct
I build small little data classes all the time and there's a reason for that: Ruby makes it trivial to do so. That's a big win because we all know that what is a trivial data class today will be tomorrow's super object, right? If I start out using a simple
Array
orHash
, I'll probably end up redoing most of the logic at both ends eventually. Or I can start with the trivial class and grow it naturally.The key to all this though is that I don't write those classes myself! That's what Ruby is for. More specifically, you need to learn to love
Struct
. Allow me to show you what I mean.Imagine I need a basic class to represent a
Contact
. Ruby gives us so many shortcuts that the class could be very small even withoutStruct
:class Contact def initialize(first, last, email) @first = first @last = last @email = email end attr_accessor :first, :last, :email end
You could shorten that up more with some multiple assignment if you like, but that's the basics. Now using
Struct
is even easier: -
9
OCT
2008Dual Interface Modules
I'm guessing we've all seen Ruby's
Math
Module
. I'm sure you know that you can call methods in it as "module (or class) methods:"Math.sqrt(4) # => 2.0
That's just one way to use the
Math
Module
though. Another is to treat it as a mixin and call the same methods as instance methods:module MyMathyThing extend Math def self.my_sqrt(*args) sqrt(*args) end end MyMathyThing.my_sqrt(4) # => 2.0
Ruby ships with a few
Module
s that work like this, including the mightyKernel
.How is this dual interface accomplished? With the seldom seen
module_function()
method. You use this much like you wouldprivate()
, to affect all following method definitions:module Greeter module_function def hello "Hello!" end end module MyGreeter extend Greeter def self.my_hello hello end end Greeter.hello # => "Hello!" MyGreeter.my_hello # => "Hello!"
As you can see, it magically gives us the dual interface for the methods beneath it. You can also affect specific methods by name, just as you could with
private()
. This is equivalent to my definition above: -
8
OCT
2008Readable Booleans
There's a great little trick you can do to improve the readability of your code. A common problem is dealing with methods that have a boolean flag arguments. Here's an example I ran into just today in a Rails application:
def rating_stars(..., clickable = false) # ... end
The problem with this is that you typically see calls like this scattered around the application:
<%= rating_stars(..., true) %>
Would you know what
true
did there if I hadn't shown you the name of the variable first? I didn't. I had to go hunting for that method definition.Ironically the opposite problem, a magical dangling
false
, is much more rare in my experience. That's typically the default for these kind of arguments and it just makes more sense and reads better to leave it out.Anyway, the point is that we can typically improve the ease of understanding the common case. Remember that in Ruby
false
andnil
are false while everything else is true. That means that truth is very loosely defined and we can pass a lot of things for our boolean flag value. For example, after looking up the method and understanding what was needed, I chose to call it like this: -
7
OCT
2008DSL Block Styles
There's an argument that rages in the Ruby camps: to
instance_eval()
or not toinstance_eval()
. Most often this argument is triggered by DSL discussions where we tend to want code like:configurable.config do width 100 mode :wrap end
You can accomplish something like this by passing the block to
instance_eval()
and changingself
to an object that defines thewidth()
andmode()
methods. Of course changingself
is always dangerous. We may have already been inside an object and planning to use methods from that namespace:class MyObject include Configurable # to get the config() method shown above def initialize config do width calculate_width # a problem: may not work with instance_eval() end end private def calculate_width # the method we want to use # ... end end
In this example, if
width()
comes from a different configuration object, we're in trouble. Theinstance_eval()
will shift the focus away from ourMyObject
instance and we will get aNoMethodError
when we try to callcalculate_width()
. This may prevent us from being able to useConfigurable
in our code. -
6
OCT
2008Conversion Methods
I want to take a step back from all the syntax I've been covering lately and just talk about some simple methods in Ruby's core. Ruby ships with so many great helpers, it's often hard to keep track of what everything can do. Specifically, let's talk about the type conversion methods.
I assume we all make calls to
to_s()
andto_i()
regularly:255.to_s # => "255" "255".to_i # => 255
There shouldn't be any surprises there. Even these two simple methods can do more though. They make it possible to convert to and from various numeric bases. For example, here are the same conversions into and out of base 16 (hexadecimal):
255.to_s(16) # => "ff" "ff".to_i(16) # => 255
Ruby has other ways to do these same conversions. Here are two unusual methods (beginning with capital letters) that are similar:
String(255) # => "255" Integer("255") # => 255
I'll be honest and tell you that I don't really find
String()
useful as it just callsto_s()
for you, butInteger()
is a different story. First of all,to_i()
is very lenient about what it converts whileInteger()
is more strict: -
3
OCT
2008I'm Addicted to the Word Array
Continuing with my recent trend of showing of fun uses of Ruby syntax, I have a confession to make: I'm addicted to Ruby's "word
Array
." I really am.I suspect most of you know this, but the word
Array
is a shortcut that can lessen the quote-comma-quote syndrome of simple a simpleArray
like:["a", "wordy", "Array"]
You can create the same
Array
with the wordArray
syntax:%w[a wordy Array]
That's essentially just a
String
that will automatically besplit()
on whitespace to build anArray
. You can use any amount of space any place you like, so you can layout the data in whatever way makes the most sense for you:require "pp" pp %w[ one two three four five six seven eight nine zero ] # >> ["one", # >> "two", # >> "three", # >> "four", # >> "five", # >> "six", # >> "seven", # >> "eight", # >> "nine", # >> "zero"]
Note that you can chose the punctuation characters used at either ends of the
Array
, some of which are paired while others just repeat: -
2
OCT
2008Interpolation and Statements
I still cringe anytime I see code like:
"1 + 2 = " + (1 + 2).to_s # => "1 + 2 = 3"
Some books even advocate the above, which is a real shame for Ruby.
I imagine most of you know that you can rewrite the above to use
String
interpolation:"1 + 2 = #{1 + 2}" # => "1 + 2 = 3"
Let's think about that simple code a little bit more than we usually do though. What's really going on here? Obviously
#{ … }
inserts the result of the embedded code in theString
, but it's important to realize that it also callsto_s()
on that result to make it fit in theString
.We can really make use of that knowledge if we try. Here's an example:
Name = Struct.new(:first, :last) do def full "#{first} #{last}".strip # trick 1 end alias_method :to_s, :full # trick 2 end Name.new("James").full # => "James" Name.new(:James, :Gray).full # => "James Gray" "My name is #{Name.new('James', 'Gray')}." # => "My name is James Gray."
I've built a trivial data class for managing names here. In that, I've tried to make use of interpolation to the fullest.
-
2
OCT
2008Working With Multiline Strings
I imagine most Rubyists are aware that Ruby has "heredocs," but do you really know all they can do? Let's find out.
A "here document" is a literal syntax for a multiline
String
. In the most basic form, they look like this:p <<END_HEREDOC This is a multiline, as is String! END_HEREDOC # >> "This is a\n multiline,\nas is String!\n"
The
<<NAME
syntax introduces the heredoc, but it actually begins at the start of the following line. It continues untilNAME
occurs again, at the beginning of a line. Note the trailing newline in the example above. All of the data between start and finish is packaged up into aString
and dropped in where the original<<NAME
designator appeared.There are some important details in that description, namely that the
String
begins on the next line and that it's inserted where the heredoc was started. This means that the rest of the line where the heredoc is started can have normal Ruby code (though your editor may syntax highlight it badly):p <<END_SQL.gsub(/\s+/, " ").strip SELECT * FROM users ORDER BY users.id DESC END_SQL # >> "SELECT * FROM users ORDER BY users.id DESC"