sourcetagsandcodes.com

Ruby Tips Part 1

Posted 2 September 2013 by Mat Sadler

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

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

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.