Ruby’s Unary Operators and How to Redefine Their Functionality
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 and.. technically you can't "redefine an operator" but Ruby's operators frequently use specially named methods behind the scenes, and as you'll know.. redefining a method is easy in Ruby!
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:
> -"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's method 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.
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
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
def !
swapcase
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"
p !str # => "tHIS IS MY STRING!"
p (not str) # => "tHIS IS MY STRING!"
p !(~str) # => "gUVF VF ZL FGEVAT!"
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 years 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, with assistance from Christoph Grabo, I 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 ;-)
October 16, 2014 at 7:22 am
It seems dangerous to override the "!" operator, because when used in conditions (if, until, ...) the condition may not evaluate as expected...
For example (in Ruby 1.9):
It seems a very practical option to overload unary operators, but I should not use it for the "!", as it may give unexpected results inside conditions
Great post :)