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 2 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 is also available and covers blocks and ranges.
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
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 Proc
s, 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.