Ruby Tips Part 2

By: Mat Sadler

Tags:

  • ruby

This is part 2 of a 5-part series on Ruby tips and tricks gleaned from our team’s pull requests over the last two years. Part 1 is also available and covers blocks and ranges.

Destructuring

You’ve probably come across Ruby’s (de|re)structuring “splat” operator before, e.g.

attrs = [:data, :cache]
attr_accessor *attrs # destructure array into an arguments list
private *attrs
def hyphenate(*words) # restructure arguments list into an array
  words.join("-")
end

This can be used in assignments too, in this next example it’s used both to collect together the central elements into body and to ‘break apart’ the range.

head, *body, tail = *(1..10)
head   #=> 1
body   #=> [2, 3, 4, 5, 6, 7, 8, 9]
tail   #=> 10

Arrays will be automatically destructured on the right hand side of an assignment, this works particularly nicely when you have a method returning an array.

family, port, host, address = socket.peeraddr

I’m also partial to this trick for grabbing just the first element of an array.

family, = socket.peeraddr

However, the same result can be achieved more clearly with #first.

family = socket.peeraddr.first

You can take advantage of this for Hashes using the #values_at method, which returns an array.

first, last = params.values_at(:first_name, :last_name)

This implicit destructuring also happens in block arguments.

names = ["Arthur", "Ford", "Trillian"]
ids = [42, 43, 44]
id_names = ids.zip(names)   #=> [[42, "Arthur"], [43, "Ford"], [44, "Trillian"]]
id_names.each do |id, name|
  puts "user #{id} is #{name}"
end

Even cooler is you can force further destructuring with parentheses.

id_names = [[42, ["Arthur", "Dent"]], [43, ["Ford", "Prefect"]], [44, ["Tricia", "McMillan"]]]
id_names.each do |id, (first_name, last_name)|
  puts "#{id}\t#{last_name}, #{first_name[0]}."
end

and this works in method parameters!

def euclidean_distance((ax, ay), (bx, by))
  Math.sqrt((ax - bx)**2 + (ay - by)**2)
end

euclidean_distance([1, 5], [4, 2])   #=> 4.242640687119285

Ruby has two methods you can implement for your own classes so that you can take advantage of this. The first to allow an object to be explicitly destructured with * is #to_a.

class Point
  attr_accessor :x, :y
  def initialize(x, y)
    @x, @y = x, y
  end

  def to_a
    [x, y]
  end
end

point = Point.new(6, 3)
x, y = *point
x   #=> 6
y   #=> 3

The second, for implicit destructuring, is #to_ary. You’ll want to be more selective in implementing this one, as it can lead to your objects suddenly behaving like arrays in places you weren’t expecting.

class Point
  attr_accessor :x, :y
  def initialize(x, y)
    @x, @y = x, y
  end

  def to_ary
    [x, y]
  end
end

point = Point.new(6, 3)
x, y = point
x   #=> 6
y   #=> 3

points = [Point.new(1, 5), Point.new(4, 2)]
points.each do |x, y|
  ...
end

# using our distance method from an earlier example
euclidean_distance(Point.new(1, 5), Point.new(4, 2))    #=> 4.242640687119285

There’s one final use for the splat (*); when overriding methods in a subclass and calling super. If it turns out you don’t need the arguments to the method for whatever additional behaviour you’re adding, you can accept all arguments with a bare splat, then a bare super passes all arguments to super.

class LoggedReader < Reader
  def initialize(*)
    super
    @logger = Logger.new(STDOUT)
  end

  def read(*)
    result = super
    @logger.info(result)
    result
  end
end

Conversion methods

We’ve just seen there are methods for implicit and explicit conversion of an object to an array. These can also be thought of as non-strict and strict conversion methods, and there are a few for other types.

The non-strict methods you probably know well, #to_a, #to_i, #to_s, as of Ruby 2.0 #to_h, and a handful of others. These are available on a wide range of classes, including nil.

{:foo => 1, :bar => 2}.to_a   #=> [[:foo, 1], [:bar, 2]]
nil.to_a                      #=> []
"3".to_i                      #=> 3
"foo".to_i                    #=> 0
nil.to_i                      #=> 0
[1,2,3].to_s                  #=> "[1, 2, 3]"
nil.to_s                      #=> ""
nil.to_h                      #=> {}

These non-strict conversions can sometimes trip you up, allowing invalid data to pass further down a system than you’d want, returning incorrect results, or producing unexpected errors.

USERS = ["Arthur", "Ford", "Trillian"]

def user(id)
  USERS[id.to_i]
end

user(nil)   #=> "Arthur" # oops! should have returned an error

Ruby provides some more strict conversion methods for a few types, #to_ary, #to_int, and #to_str. These are only implemented on classes where the strict conversion makes sense, e.g. #to_int is only available on numeric classes.

def user(id)
  USERS[id.to_int]
end

user(nil)   #=> NoMethodError: undefined method ‘to_int’ for nil:NilClass

This makes our example a little better, we’re getting an error and it’s coming from the right place, but the error doesn’t really convey our full intent. Plus the original was probably written that way to allow strings to be used, and as String doesn’t implement #to_int we’ve broken that.

But Ruby has another set of conversion methods, and these are a little more intelligent. The most useful of these are Array() and Integer() (along with the other numeric conversion methods like Float()). String() and Hash() are less useful as they just delegate to #to_s and #to_h.

def user(id)
  USERS[Integer(id)]
end

user(nil)   #=> TypeError: can't convert nil into Integer

Now our example raises an error that really shows our intentions, plus we can use strings again. This is where the Integer() method really shines

"1".to_i         #=> 1
Integer("1")     #=> 1

"foo".to_i       #=> 0
Integer("foo")   #=> ArgumentError: invalid value for Integer(): "foo"

Array() is also really useful, as when it can’t convert its argument to an array it will put the argument in an array.

Array([1, 2])   #=> [1, 2]
Array(1)        #=> [1]

Occasionally you want to write a method that takes a single item, or an array of items, and Array() lets you do this without messing about checking types.

def project_cost(hours, developer)
  developers = Array(developer)
  avg_rate = developers.inject(0) {|acc, d| acc + d.rate} / developers.length
  hours * avg_rate
end

As we saw earlier with #to_ary some of these conversion methods are used internally to Ruby, these are the strict conversion methods, but can also be thought of as the implicit conversion methods. They can be used implicitly because they are strict.

These are used all over the place within Ruby, but as an example #to_int is used to convert the argument to Array#[] to an int, and #to_str is used by raise when its argument isn’t an Exception.

class Line
  def initialize(id)
    @id = id
  end
  def to_int
    @id
  end
end

line = Line.new(2)
names = ["Central", "Circle", "District"]
names[line]   #=> "District"
class Response
  def initialize(status, message, body)
    @status, @message, @body = status, message, body
  end

  def to_str
    "#{@status} #{@message}"
  end
end

res = Response.new(404, "Not Found", "")
raise res   #=> RuntimeError: 404 Not Found

One final conversion method to mention is #to_proc. This is used by the unary & operator in method arguments. & converts a Proc object into a block argument in a method call.

sum = Proc.new {|a, b| a + b}
(1..10).inject(&sum)   #=> 55

But first unary & will call #to_proc on its operand, this allows it to be used with objects that aren’t Procs, but can be converted to one. A very common use of this that you’ll find in modern Ruby code is using this with a Symbol.

["foo", "bar", "baz"].map(&:upcase)   #=> ["FOO", "BAR", "BAZ"]

This has been built in to Ruby since version 1.9, but before that you could implement it yourself with some code like this:

class Symbol
  def to_proc
    Proc.new do |obj|
      # call the method named by this symbol on the supplied object
      obj.send(self)
    end
  end
end

Here’s an example of using #to_proc and & to simplify initialising Points from an array.

class Point
  attr_accessor :x, :y
  def initialize(x, y)
    @x, @y = x, y
  end

  def self.to_proc
    Proc.new {|ary| new(*ary)}
  end
end

[[1, 5], [4, 2]].map(&Point)   #=> [#<Point:0x007f87e983af40 @x=1, @y=5>, #<Point:0x007f87e983ace8 @x=4, @y=2>]

Head on to Part 3.


About the Author

Mat Sadler

blog comments powered by Disqus