This article was originally posted on the globaldev blog, they have kindly allowed me to repost it here. If you’re looking for a Ruby job in London you should check out their jobs page.
The team at Global Personals have been been using GitHub and following the GitHub Flow for about 2 years now. In that time we’ve clocked up a few thousand pull requests, and with the wide range of experience here it’s rare for one to get though without a comment or two on how things could be improved. While a lot of the advice is project specific, there’s also loads of great little tips applicable to any Ruby codebase shared between the team.
Not wanting this to get lost I’ve waded though a big chunk of our Pull Requests, picked out the best bits to share, and expanded on them a little. Everyone has their own way of working and style so I’ve also tried to keep this descriptive, rather than prescriptive. Not everything will be new to everyone, but hopefully there will be something helpful in here for you.
I’ve split things up in to parts to save you reading 5000 words all in one go, and grouped things in to sections for easy reference.
Let’s dive in to part 1.
Blocks are a key part of Ruby; you see them used all the time. You don’t, however, see many people writing methods that take blocks, even when it could really clean up the code.
There are 3 main uses for blocks: looping, setup and teardown, and callbacks or deferred actions.
Here’s an example that loops through all the fibonacci numbers. It uses the
block_given?
method to check if a block has been supplied, and if not return
an Enumerator generated from the current method.
The yield
keyword transfers control to the block, its argument becoming the
argument to the block. Once the block has run, control transfers back to the
method, and the next line of code is executed. It finishes by returning the last
value below the maximum.
def fibonacci(max=Float::INFINITY)
return to_enum(__method__, max) unless block_given?
yield previous = 0
while (i ||= 1) < max
yield i
i, previous = previous + i, i
end
previous
end
fibonacci(100) {|i| puts i}
The next example puts all the setup, teardown, and error handling code in a single method that yields to a block to then do the work. This way the boilerplate code need not be repeated all over the place, plus there’s only one bit of code you need to change when you want to tweak that error handling.
Here the return value of the yield
statement is captured into a local
variable; this is the value returned from the block. In this case it makes sense
for this to be the return value of the method.
require "socket"
module SocketClient
def self.connect(host, port)
sock = TCPSocket.new(host, port)
begin
result = yield sock
ensure
sock.close
end
result
rescue Errno::ECONNREFUSED
end
end
# connect to echo server, see next example
SocketClient.connect("localhost", 5555) do |sock|
sock.write("hello")
puts sock.readline
end
The next example doesn’t use the yield
keyword. There’s another way of
working with blocks: prefixing the last argument in a method’s argument list
with &
will capture the block into that argument as a Proc object. Procs have
a #call
method you can use to execute the block, arguments passed to #call
become the block arguments. You can use this to store the block and run it
later as a callback in this example, or a deferred action run later only when
it’s needed.
require "socket"
class SimpleServer
def initialize(port, host="0.0.0.0")
@port, @host = port, host
end
def on_connection(&block)
@connection_handler = block
end
def start
tcp_server = TCPServer.new(@host, @port)
while connection = tcp_server.accept
@connection_handler.call(connection)
end
end
end
server = SimpleServer.new(5555)
server.on_connection do |socket|
socket.write(socket.readline)
socket.close
end
server.start
Ranges are a common sight in Ruby code, usually showing up in the form
(0..9)
, that is an inclusive range of 0 up to and including 9. There’s also
an exclusive form, (0...10)
, 0 through all values less than 10, the result is
the same. This form isn’t seen as often, but sometimes comes in useful.
Occasionally you might see something like:
random_index = rand(0..array.length-1)
This is much tidier with an exclusive range:
random_index = rand(0...array.length)
There are also cases where it’s much easier, or makes more sense, to define
your range with a boundary that’s not in the range. feb_first...march_first
is much easier than working out how many days there are in February this year,
and 1...Float::INFINITY
makes more sense as a range of positive integers, as
infinity isn’t a number.
Ranges are great, as they allow us to define large sets without actually creating the whole set in memory. Some operations on Ranges require that whole set to be created though, so you have to be careful of the methods you use.
#each
obviously has to create the whole Range, but it does so only 1 object at
a time, this way (1...Float::INFINITY).each {|i| puts i}
is actually possible,
it won’t use up all available memory before outputting anything (although it
won’t ever terminate). The methods Range gets from Enumerable all delegate to
#each
, so behave the same.
Range has two methods to check if a value is in the range, #include?
and
#cover?
. #include?
uses #each
to iterate though the range, to check if
the object is there, #cover?
simply checks the object is greater than the
beginning, or less than or equal to the end of the range (or just less than for
an exclusive range). The methods aren’t quite interchangeable though, due to
some quirks of sorting order and how ranges are built.
("a".."z").include?("ab") #=> false
("a".."z").cover?("ab") #=> true
Ranges work with many of Ruby’s built in classes, and it’s really simple to allow your classes to work with Range too.
First you’ll need to implement the <=>
‘spaceship’ operator method. This
should return -1
if the other is greater, 1
if the other is lesser, or 0
if the other is equal. The standard behaviour is to return nil
if the
comparison isn’t valid. In the following example it’s simply delegated to
String#casecmp
, which does a case-insensitive comparison on string, returning
results in the same format.
class Word
def initialize(string)
@string = string
end
def <=>(other)
return nil unless other.is_a?(self.class)
@string.casecmp(other.to_s)
end
def to_s
@string
end
end
This lets you build a range, and test for membership using #cover?
but that’s
pretty much it.
dictionary = (Word.new("aardvark")..Word.new("xylophone"))
dictionary.cover?(Word.new("derp")) #=> true
If you want to be able to iterate the Range, generate an array, or test for
true membership using #include?
you’ll need to implement #succ
. This should
return the next object in the sequence.
class Word
DICTIONARY = File.read("/usr/share/dict/words").each_line.map(&:chomp)
DICTIONARY_INDEX = (0...DICTIONARY.length)
include Comparable
def initialize(string, i=nil)
@string, @index = string, i
end
def <=>(other)
return nil unless other.is_a?(self.class)
@string.casecmp(other.to_s)
end
def succ
i = index + 1
string = DICTIONARY[i]
self.class.new(string, i) if string
end
def to_s
@string
end
private
def index
return @index if @index
if DICTIONARY_INDEX.respond_to?(:bsearch) # ruby >= 2.0.0
@index = DICTIONARY_INDEX.bsearch {|i| @string.casecmp(DICTIONARY[i])}
else
@index = DICTIONARY.index(@string)
end
end
end
You’ll notice I also included the Comparable module, this generates a sensible
#==
(and #<
, #>
, etc) method from the #<=>
method we defined, rather
than using the default which is based on object identity.
Our Range is now much more useful.
dictionary = (Word.new("aardvark")..Word.new("xylophone"))
dictionary.include?(Word.new("derp")) #=> false
(Word.new("town")..Word.new("townfolk")).map(&:to_s) #=> ["town", "towned", "townee", "towner", "townet", "townfaring", "townfolk"]
Head on to Part 2.