-
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
. -
30
JAN
2015Random Access Terminal
I've recently been playing around with fancy terminal output in Ruby. I've learned quite a bit about this arcane magic. I've also realized that the documentation is pretty spotty. I want to see if I can improve that with a few blog posts, so let's dive right in.
Output
Program output typically happens in a linear order from top to bottom. For example, this code:
puts "onez" puts "twos" puts "threes"
generates this output:
onez twos threes
But what if you need to change some output? Could you replace the
z
above with ans
if you needed to? Yes, but it can get a little involved.ANSI Escape Codes
In many cases, we just push some characters to
$stdout
(the streamKernel#puts
is writing to above) and your terminal program happily shows them to the user. However, your terminal is watching these characters for special sequences that it understands. Some of those sequences of characters can cause your terminal to take actions other than just writing some output to the screen. -
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. -
11
MAY
2012Delaying Decisions
I love playing with Ruby's
Hash
. I think it has a neat API and experimenting with it can actually help you understand how to write good Ruby. Let's dig into this idea to see what I mean.The nil Problem
In Destroy All Software #9 Gary chooses to show an example in Python because, unlike Ruby's
Hash
, it will raise an error for a non-existent key. Ruby just returnsnil
, he explains.What Gary said isn't really true, but I'm guessing he just didn't know that at the time. He was in the process of switching to Ruby from Python and I'm guessing he just didn't have a deep enough understanding of Ruby's
Hash
yet. I bet he does know how it works now.But assume he was right. What's he saying and why does it matter? Consider some code like this:
class SearchesController < ApplicationController def show terms = params[:terms] SomeModel.search(terms) # ... end end
This is what Gary doesn't like, and rightfully so. Because I indexed into
params
here with the[]()
method, I will indeed get anil
if the:terms
key wasn't inparams
. -
13
OCT
2008The Secret Shell Helper
Someone pops onto the Ruby Talk mailing list fairly regularly asking how to break up content like:
one "two" "a longer three"
They expect to end with a three element
Array
, where the third item will contain spaces. They generally expect the quotes will have been removed as well.If your needs are very, very simple you may be able to handle this with a regular expression:
data = 'one "two" "a longer three"' p data.scan(/"([^"]*)"|(\S+)/).flatten.compact # >> ["one", "two", "a longer three"]
That just searches for either a set of quotes with some non-quote characters between them or a run of non-whitespace characters. Those are the two possibilities for the fields. Note that the two separate capture here mean
scan()
will returns contents in the form:[[nil, "one"], ["two", nil], ["a longer three", nil]]
That's why I added a
flatten()
andcompact()
to get down to the actual matches.The regular expression approach can get pretty complex though if any kind of escaping for quotes is involved. When that happens, you may need to step up to a parser.
-
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: -
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
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"
-
1
OCT
2006I Just Want One Character!
Every so often a person asks the question on Ruby Talk, "How can I get just one character from the keyboard (without needing the user to hit return)?" Everyone is always quick to post solutions, but sadly there are some issues with almost every one of them.
The general consensus is that this is a tough problem to solve correctly. I say that's the exact reason to let HighLine handle this for you:
#!/usr/bin/env ruby -w require "highline/system_extensions" include HighLine::SystemExtensions print "Enter one character: " char = get_character puts char.chr
That doesn't look too tough, does it?
What's terrific about this solution is that under-the-hood
HighLine
will check your platform and libraries and then try to use the solution that makes the most sense for your environment. The code is really pretty robust too, because people a lot smarter than me have been sending in patches for over a year, slowly eliminating all of those tricky edge cases.As you can see, I've split this functionality of
HighLine
into a separate module so you don't even need to load the fullHighLine
system. This was done just because this is such a real and common problem. This section ofHighLine
is one pure Ruby file, so feel free to vendor it if the external dependency is an issue. -
30
JUL
2006PStore Meets YAML
I love the
PStore
standard library. It's a very graceful interface to get some fairly robust serialized mini-database handling in just a few lines. With it you get:- Transactions with commit and rollbacks (automatic on exception).
- File locking, shared and exclusive.
- Multiprocessing safety.
PStore
does even more, including some file mode checking and MD5 hashing to avoid unneeded writes, but the above are the major selling points for me.Now, if I had to level any one complaint at
PStore
, it would be that because it usesMarshal
under the hood it doesn't create files you can easily browse or tweak by hand. (Marshal
is a feature, don't get me wrong. It's fast, which is very helpful.) Sometimes though I wantPStore
protection with theYAML
file format.I'm embarrassed to admit that I use to use a hack for this:
require "pstore" require "yaml" class PStore; Marshal = YAML; end
That just redefines the
Marshal
constant in a scope that should only alterPStore
. The library only usesdump()
andload()
and those methods work the same withMarshal
andYAML
.