Ruby’s Unary Operators and How to Define Their Functionality
by Peter Cooper
at 2011-11-09 01:50:38
original http://feedproxy.google.com/~r/RubyInside/~3/B4Wup3_1oH4/rubys-unary-operators-and-how-to-redefine-their-functionality-5610.html
In math, a unary operation is an operation with a single input. In Ruby, a unary operator is an operator which only takes a single 'argument' in the form of a receiver. For example, the -
on -5
or !
on !true
.
In contrast, a binary operator, such as in 2 + 3
, deals with two arguments. Here, 2 and 3 (which become one receiver and one argument in a method call to +
).
Ruby only has a handful of unary operators, and while it's common to redefine binary operators like +
or []
to give your objects some added syntactic sugar, unary operators are less commonly redefined. In my experience, many Rubyists aren't aware that unary operators can be redefined, so this article demonstrates how.
A Quick Example with -@
Let's ease into things with the -
unary operator. The -
unary operator is not the same thing as the - binary operator (where a binary operator has two operants). By default, the -
unary operator is used as notation for a negative number, as in -25
, whereas the -
binary operator performs subtraction, as in 50 - 25
. While they look similar, these are different concepts, different operators, and resolve to different methods in Ruby.
Using the - unary operator on a string in irb:
ruby-1.9.3-p0 :001 > -"this is a test"
NoMethodError: undefined method `-@' for "this is a test":String
The String class doesn't have unary -
defined but irb gives us a clue on where to go. Due to the conflict between the unary and binary versions of -
, the unary version has a suffix of @. This helps us come up with a solution:
str = "This is my STRING!"
def str.-@
downcase
end
p str # => "This is my STRING!"
p -str # => "this is my string!"
We've defined the unary -
operator by defining its associated -@
method to translate its receiving object to lower case.
Some Other Operators: +@, ~, ! (and not)
Let's try a larger example where we subclass String and add our own versions of several other easily overridden unary operators:
class MagicString < String def +@ upcase end def -@ downcase end def ! swapcase end def ~ # Do a ROT13 transformation - http://en.wikipedia.org/wiki/ROT13 tr 'A-Za-z', 'N-ZA-Mn-za-m' end end str = MagicString.new("This is my string!") p +str # => "THIS IS MY STRING!" p !str # => "tHIS IS MY STRING!" p (not str) # => "tHIS IS MY STRING!" p ~str # => "Guvf vf zl fgevat!" p +~str # => "GUVF VF ZL FGEVAT!" p !(~str) # => "gUVF VF ZL FGEVAT!"
This time we've not only redefined -/-@
, but the +
unary operator (using the +@
method), !
and not
(using the !
method), and ~
.
I'm not going to explain the example in full because it's as simple as I could get it while still being more illustrative than reams of text. Note what operation each unary operator is performing and see how that relates to what is called and what results in the output.
Note: You cannot redefine !
in Ruby 1.8. This code performs as-is in Ruby 1.9 only. If you remove the !
method and examples, the code otherwise works in Ruby 1.8. There's a longer 1.8 and 1.9 example later in this post.
Special Cases: & and *
&
and *
are also unary operators in Ruby, but they're special cases, bordering on 'mysterious syntax magic.' What do they do?
& and to_proc
Reg Braithwaite's The unary ampersand in Ruby post gives a great explanation of &
, but in short & can turn objects into procs/blocks by calling the to_proc
method upon the object. For example:
p ['hello', 'world'].map(&:reverse) # => ["olleh", "dlrow"]
Enumerable#map
usually takes a block instead of an argument, but &
calls Symbol#to_proc
and generates a special proc object for the reverse
method. This proc becomes the block for the map
and thereby reverses the strings in the array.
You could, therefore, 'override' the &
unary operator (not to be confused by the equivalent binary operator!) by defining to_proc
on an object, with the only restriction being that you must return a Proc object for things to behave. You'll see an example of this later on.
* and splatting
There's a lot of magic to splatting but in short, *
can be considered to be a unary operator that will 'explode' an array or an object that implements to_a
and returns an array.
To override the unary *
(and not the binary * - as in 20 * 32
), then, you can define a to_a
method and return an array. The array you return, however, will face further consequences thanks to *'s typical behavior!
A Full Example for Ruby 1.8 and Ruby 1.9
We've reached the end of our quick tour through Ruby's unary operators, so I wanted to provide an example that shows how to override (or partially override) them that should stand as its own documentation:
class MagicString < String def +@ upcase end def -@ downcase end def ~ # Do a ROT13 transformation - http://en.wikipedia.org/wiki/ROT13 tr 'A-Za-z', 'N-ZA-Mn-za-m' end def to_proc Proc.new { self } end def to_a [self.reverse] end if RUBY_VERSION > "1.9.0" eval %{def ! swapcase end} end end str = MagicString.new("This is my string!") p +str # => "THIS IS MY STRING!" p ~str # => "Guvf vf zl fgevat!" p +~str # => "GUVF VF ZL FGEVAT!" p %w{a b}.map &str # => ["This is my string!", "This is my string!"] p *str # => "!gnirts ym si sihT" if RUBY_VERSION > "1.9.0" p !str # => "tHIS IS MY STRING!" p (not str) # => "tHIS IS MY STRING!" p !(~str) # => "gUVF VF ZL FGEVAT!" end
It's almost a cheat sheet of unary operators :-)
A Further Example: The TestRocket
TestRocket is a tiny testing library I built for fun a few months ago. It leans heavily on unary operators. For example, you can write tests like this:
+-> { Die.new(2) }
--> { raise }
+-> { 2 + 2 == 4 }
# These two tests will deliberately fail
+-> { raise }
--> { true }
# A 'pending' test
~-> { "this is a pending test" }
# A description
!-> { "use this for descriptive output and to separate your test parts" }
The -> { }
sections are just Ruby 1.9 style 'stabby lambdas' but I've (with assistance from Christoph Grabo) added unary methods to them so that you can prefix +
, -
, ~
, or !
to get different behaviors.
Hopefully you can come up with some more useful application for unary methods on your own objects, of course!