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 returns
nil, 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
Hashyet. 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
paramshere with the
()method, I will indeed get a
:termskey wasn't in
The 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
compact()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.
All 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
Hash, 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 without
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
Structis even easier:
I'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
Arrayis a shortcut that can lessen the quote-comma-quote syndrome of simple a simple
["a", "wordy", "Array"]
You can create the same
Arraywith the word
%w[a wordy Array]
That's essentially just a
Stringthat will automatically be
split()on whitespace to build an
Array. 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:
Working 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"
<<NAMEsyntax introduces the heredoc, but it actually begins at the start of the following line. It continues until
NAMEoccurs 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 a
Stringand dropped in where the original
There are some important details in that description, namely that the
Stringbegins 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"
I 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
HighLinewill 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
HighLineinto a separate module so you don't even need to load the full
HighLinesystem. This was done just because this is such a real and common problem. This section of
HighLineis one pure Ruby file, so feel free to vendor it if the external dependency is an issue.
PStore Meets YAML
I love the
PStorestandard 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.
PStoredoes 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 uses
Marshalunder the hood it doesn't create files you can easily browse or tweak by hand. (
Marshalis a feature, don't get me wrong. It's fast, which is very helpful.) Sometimes though I want
PStoreprotection with the
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
Marshalconstant in a scope that should only alter
PStore. The library only uses
load()and those methods work the same with
String Has Other Methods Besides =~/match() and sub()
Ask anyone who knows me and they will tell you I'm a huge fan of regular expressions. I use them all the time and my
FasterCSVlibrary is a regular expression powered parser. However, even I know they are not for everything, and lately I keep running into almost comical examples of misuse. Here are some of my favorites.
First, we have:
str =~ /=/
That snippet is like calling for a military escort (the regular expression engine) to see you safely to the grocery store down the block. That's fun, but probably overkill. In this case, a call to
include?()will do the trick:
That may be more like riding your bike to the grocery store, but it gets the job done and is a bit faster to boot.
Funny example number two. I've seen this before:
str =~ /\Aquit\Z/
Again, the regular expression engine appreciates the love, but you really just want
str == "quit"
Even for some of the fancier stuff, you don't need a full blown regular expression. For example, this:
The Books are Wrong About Logger
I've read several books that introduced the standard
Loggerlibrary and they all agree on one thing: you can't customize the output. That's so last version in thinking! Behold…
Here's a trivial
Loggerscript, showing basic functionality:
#!/usr/bin/env ruby -w require "logger" def expensive_error_report sleep 3 # Heavy Computation Simulation (patent pending) "YOU BROKE IT!" end log = Logger.new(STDOUT) log.level = Logger::INFO # set out output level above the DEBUG default log.debug("We're not in the verbose debug mode.") log.info("We do see informative logs though.") if log.error? # check that this will be printed, before waste time log.error(expensive_error_report) end
If you run that you will see:
I, [2006-07-08T11:17:19.531943 #340] INFO -- : We do see informative logs though. E, [2006-07-08T11:17:22.532424 #340] ERROR -- : YOU BROKE IT!
Now everyone has always known you can format the date and time display using a
#!/usr/bin/env ruby -w require "logger" def expensive_error_report sleep 3 "YOU BROKE IT!" end log = Logger.new(STDOUT) log.level = Logger::INFO log.datetime_format = "%Y-%m-%d %H:%M " # simplify time output log.debug("We're not in the verbose debug mode.") log.info("We do see informative logs though.") if log.error? log.error(expensive_error_report) end