sourcetagsandcodes.com

Ruby Tips Part 4

Posted 14 October 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.

This is part 4 of a 5-part series on Ruby tips and tricks gleaned from the Global Personals team’s pull requests over the last two years. Part 1 covers blocks and ranges, part 2 deals with destructuring and type conversions, and part 3 talks about exceptions and modules.

Debugging

The Rails console is really useful for interactive debugging, and the same approach can come in very handy for non-Rails projects. Using irb from the the stdlib it’s surprisingly easy to get something up and running for your own project.

This example assumes you’ve put your code in lib/ and your project has a sensible entry point that you can require to load up all the code.

I’ve added an example of setting up a Sequel database connection, you can replace this with whatever setup code you might need, or just remove it. You can also remove the require "bundler/setup" if you’re not using Bundler.

#!/usr/bin/env ruby

require "bundler/setup"
require_relative "lib/my_project"
require "sequel"
require "irb"

def config
  return @config if @config
  config_path = File.expand_path("../config/application.yml", __FILE__)
  @config = YAML.load_file(config_path)
end

DB = Sequel.connect(config[:database])

IRB.start

Save this in the base directory of you project as console and run chmod +x console on the command line to make the file executable. You can now start up a console session with ./console in the base directory of your project.

Any methods, instance variables, or constants you declare before the IRB.start will be available during the console session, but it creates a new scope so local variables aren’t accessible. Anything after will be run once you quit irb.

When you’re debugging on the console irb shows you the result of each expression by calling #inspect on the result. Usually this gives you a fairly low-level representation helpful in debugging, however in the case of Floats it’s not quite as useful as it could be. Due to the way Ruby (and indeed most programming languages) represent floating point numbers they are rarely exactly the value you specify. An easy way to see the true value is using sprintf

sprintf("%.50f", 1.115)   #=> "1.11499999999999999111821580299874767661094665527344"

Other times you can have the opposite problem, the detailed output from #inspect can obscure what it is you’re actually looking for.

require "bigdecimal"

decimals = [BigDecimal("1.0")]
5.times {decimals << decimals.last / 2}
decimals   #=> [#<BigDecimal:7fbdb8871928,'0.1E1',9(18)>, #<BigDecimal:7fbdb88717c0,'0.5E0',9(36)>, #<BigDecimal:7fbdb88716a8,'0.25E0',9(36)>, #<BigDecimal:7fbdb8871590,'0.125E0',9(36)>, #<BigDecimal:7fbdb88714a0,'0.625E-1',9(36)>, #<BigDecimal:7fbdb8871360,'0.3125E-1',9(36)>]

In this case you can redefine #inspect on the class in hand in your test or console script to return something more intuitive.

class BigDecimal
  def inspect
    "#{to_s("F")}d"
  end
end

decimals   #=> [1.0d, 0.5d, 0.25d, 0.125d, 0.0625d, 0.03125d]

For the same reason it can often be helpful to define a custom #inspect on your own classes.

Sometimes you’re trying to hunt down a bug and you’re faced with two outputs nearly identical, but not quite. You don’t want to hunt though the output yourself, but using diff seems like a lot of effort when you have to save each output to a file, especially when you’re constantly changing things and running the code again to narrow the problem down. Fortunately it turns out Ruby has a handy diff method hidden away in the stdlib.

require "minitest/unit"
include Minitest::Assertions

a = ["foo", "bar", "baz"]
b = ["food","bar", "baz"]

puts diff(a.join("\n"), b.join("\n"))

outputs

--- expected
+++ actual
@@ -1,3 +1,3 @@
-"foo
+"food
 bar
 baz"

As a highly dynamic language, one that allows you to change almost anything at any time, Ruby sometimes makes it embarrassingly tricky to work out where a method is actually defined. It turns out Ruby can keep track of all this for you.

require "set"

array = [1,2,3]
m = array.method(:to_set) # get ahold of an object representing the to_set method
m.owner                   #=> Enumerable
m.source_location         #=> ["~/.rbenv/versions/2.0.0-p247/lib/ruby/2.0.0/set.rb", 635]

This shows us the #to_set method on an Array instance comes from the Enumerable module, and is actually defined on line 635 of set.rb from the stdlib.

Project Layout

There’s no set way to layout your Ruby project, but there are a few things that are good practice if you want to package your code up as a gem, and make sense for other projects too.

RubyGems works by adding a directory for each gem to the load path (the list to search for files when you require). You don’t want this to be the base directory of your project, as you’d then make all the ancillary files (Gemfile, tests, build scripts, etc) in your gem available to any users. To work around this you usually put your code in a directory called lib, and set the require_path attribute in your gemspec to "lib".

However now everything in your lib directory can now potentially be loaded with a require "name". This is fine for your core class or module, but say you have an api.rb file, this is a potentially common name and may clash with other gems or the user’s code. For this reason it’s a good idea to have a single main .rb file in your lib directory, and then have a name-spaced directory within this for the rest of your code.

Any ancillary files can go in your base directory, and most people will have a test (or spec) directory in there too.

This gives us the layout:

my_project/
  lib/
    my_project/
      bar.rb
      foo.rb
    my_project.rb
  test/
    my_project_test.rb
  console
  Gemfile
  my_project.gemspec
  Rakefile

Usually you’d use one file per class, and name the file for the class, snake_case rather than CamelCase. Directories under lib would match your name-spaces, thus you’d expect lib/my_project/foo.rb to look something like the following.

module MyProject
  class Foo
    # ...
  end
end

I’ll often have a couple of extra files that don’t map directly to a class, a lib/my_project/errors.rb that contains all the Exception classes for my project, and sometimes a lib/my_project/constants.rb that contains constants being used across the project.

Don’t feel the need to strictly follow the ‘one class per file’ rule, if you have a small helper class only used by one other it’s fine to bundle it in with the other.

When it comes to requiring files within your project you’ll want to use require_relative. require is great for loading up other gems and things from the stdlib, but as it searches the global variable $: (or $LOAD_PATH) for the thing you have required you can’t determine exactly what it’s going to do. require_relative always follows the path starting from the file it’s called in, meaning that no matter what it will always find the same file. This is especially useful in your tests, as then you know for sure which bits of code you are testing.

It’s usually a good idea to require all your classes, or at least the core ones, from your lib/my_project.rb file. This way if packaged up as a gem or when writing tests, scripts, etc there’s a single ‘entry point’ file to require and you get everything.

Documentation

Ruby comes packaged with some great documentation, and a handy little tool for navigating it called ri. It’s really simple to use, just run ri at the command line. Once it’s running you can enter a class name to get an overview of that class and a list of methods. You can view class methods with ClassName.method, and instance methods with ClassName#method

The docs for any gems you have installed will also be available though ri, assuming the authors have written any.

Documentation for gems can also be viewed by running gem server and visiting http://0.0.0.0:8808.

You can build the documentation for you own code using the rdoc command, just supply it with the directory of your code, and any additional files. It will output to a doc folder, open doc/index.html in your web browser to see what it has generated.

rdoc lib README.rdoc --main README.rdoc

This will give you an outline of your classes and methods, but you can make the documentation much more helpful to others by writing comments using the rdoc format. These comments become the descriptions of classes and methods in the rdoc output.

module Namespace # :nodoc: so we don't have an empty doc page for the namespace

  # The Example class is an example of rdoc
  # 
  # Link to a real
  # example[https://github.com/globaldev/going_postal/blob/master/lib/going_postal.rb]
  #--
  # double minus hides docs from here on, we can un-hide with double plus
  #++
  # 
  # = Heading
  # 
  # == Sub Heading
  # 
  # Indent example code 2 spaces (3 total from the #)
  # 
  #   example = Example.new
  #   baz = example.foo(bar)
  # 
  class Example
    
    # :call-seq: Example.new(arg) -> example
    # 
    #--
    # The :call-seq: directive lets you specify a custom example of how the
    # method is called, if you don't provide one the method name and arguments
    # are taken from the definition, and no return value is specified.
    #++
    # 
    # This is the initialize method, it gets documented as the +new+ class
    # method.
    # 
    # Returns a new Example instance.
    # 
    def initialize(arg)
      
    end

    # :section: demonstration methods

    # :call-seq: example.foo(bar) -> baz
    # 
    # foos the bar, returning baz
    # 
    def foo(bar)
      
    end

    # :call-seq:
    # example.qux(params) -> array or nil
    # example.quux(params) -> array or nil
    # 
    # Available params are
    # [foos] an array of Foos
    # [bar]  a Bar instance
    # [baz]  a Baz instance (optional)
    # 
    def qux(params)
      
    end
    alias quux qux

    # :section: block methods

    # :call-seq: example.each {|foo| block} -> example
    def each
      yield foo
      self
    end
  end
end

Head on to Part 5.