Last week I gave a talk at Melbourne Ruby involving some card tricks and Ruby trickery.
The talk is up on Youtube:
I do some card tricks for about 10 minutes, and then some Ruby tricks. I won’t reveal how I did the card tricks in this blog post, but I will reveal how I did the Ruby tricks.
The Ruby trickery starts at about the 9m30s mark in that video. You can see the code for most of these tricks at radar/trickery on GitHub.
In this post, I’ll cover all the tricks I had planned for the talk, not just the ones I showed off.
Please, do not use any code in this blog post in production systems. It can cause weird behaviour. Especially the one involving JavaScript sorting.
The point of this post is to show the things Ruby is capable of.
Numbers
Addition
My first trick was to override addition, which I did with this code:
class Integer
alias_method :old_plus, :+
# 2 + 2 = 6
def +(num)
old_plus(old_plus(num))
end
This allowed me to call 2 + 2
and get a result of 6
. It runs two plus operations, effectively doubling the number on the left before adding the number on the right. This is why 2 + 5 = 9
and 3 + 7 = 13
. By aliasing the original +
method using old_plus
, we can override the +
method, but still use the old one.
Multiplication
The next trick applies to multiplication, but is a little more advanced:
class Integer
alias_method :old_multiply, :*
def *(num)
m = method(:old_multiply).unbind
m.bind(3).(num / self)
end
end
This code calls method(:old_multiply)
, but unbind
s it from self
. I can then rebind this method to anything else with the bind
method, before calling it again. This re-binding makes the number 3
always the self
within the old_multiply
method, regardless of what is passed through on the left. However, the self
reference inside this override will still be the left-hand-side number.
Some examples:
3 * 3 == 3
6 * 3 == 0
99 * 999 == 30
Unary minus
The unary methods are often ignored within Ruby, despite having their uses. There’s a good Ruby Inside article about unary methods. You should read it.
The TL;DR is that unary methods work as prefixed method calls on particular objects within Ruby. I’ll show you what I mean. But let’s look at the override first:
def -@
+self
end
This is pretty straight forward. When we’re told to minus something, make it a positive instead.
What’s interesting here is that it won’t work on negative numbers straight off the bat:
>> -5
=> -5
This is because negative numbers are… well, they’re negative numbers. The minus symbol there isn’t a unary method call.
But things change if you assign a variable:
>> a = 5
=> 5
>> -a
=> 5
The number remains positive, even though it should’ve been negated. This is because this code is calling the -@
unary method.
What’s fun with this is that you can keep chaining negative signs, positive signs or a combination of both:
>> --------a
=> 5
>> ++++++++a
=> 5
>> +-+-+a
=> 5
All of these examples either call -@
, +@
or a combination of both multiple times.
Arrays
Sorting, the JavaScript (aka “right”) way
JavaScript sorting is a well-known case of… well, weird behaviour. For example, this code:
[-2, -1, 0, 1, 2].sort()
Should maintain the same order of the numbers, increasing left-to-right. Instead, the output is this:
[-1, -2, 0, 1, 2]
This “weird behaviour” happens because JavaScript sorts objects based on their string versions. The string “-1” comes before “-2”, but “-2” comes before “0”, and so on. This is specified in the EcmaScript specification (5.1), Section 15.4.4.11, but you need a PhD or higher qualification (read: galaxy brain) to understand exactly what it is saying.
How does this relate to Ruby? Well, first of all Ruby does the sorting order correctly:
>> [-2, -1, 0, 1, 2].sort
=> [-2, -1, 0, 1, 2]
But if we wanted to bring JavaScript style sorting to Ruby, then we can use this code:
module JSSort
def self.included(base)
base.alias_method :old_sort, :sort
end
def sort
self.map(&:to_s).old_sort.map(&:to_i)
end
end
Array.include(JSSort)
This converts each element to a string via map
, then sorts them using the old sorting behaviour (the default Ruby way), before converting them all back to integers.
We’re not guaranteed to have arrays of integers at all times, so we might want to put a guard around that to check at least the first element is a number:
def sort
if first.is_a?(Numeric)
self.map(&:to_s).old_sort.map(&:to_i)
else
self.old_sort
end
end
This doesn’t prevent against arrays that contain a mix of datatypes (numbers and strings), but only really nefarious people create those, and there aren’t many of those in the Ruby community so I think we can be safe here.
This code will now make Ruby sort “correctly” – at least according to JavaScript:
>> [-2, -1, 0, 1, 2].sort
=> [-1, -2, 0, 1, 2]
If we include this module into Range
, we can get the same delicious behaviour:
Range.include(JSSort)
Let’s try it:
>> (-2..2).sort
=> [-1, -2, 0, 1, 2]
Now we can make Ruby sort the same way as JavaScript.
Double Equality
A little known fact is that the ==
in code like [1,2,3] == [1,2,3]
is actually a method call. This code calls Array#==
, and we can override this method too.
class Array
alias_method :old_double_equals, :==
def ==(other)
method(:old_double_equals).(other) ? "yes" : "no")
end
end
Rather than getting the plain (and boring) true
or false
when we compare arrays, we will now get “yes” or “no”.
>> [1,2,3] == [1,2,3]
=> "yes"
>> [1,2,3] == [1,2]
=> "no"
You can make this method a little more fun by first checking the length and then determining what to do on that:
other.length > 3 ? "maybe" : (method(:old_double_equals).(other) ? "yes" : "no")
This way then, you get “maybe” if you try to compare against an array of more than 3 elements:
>> [1,2,3] == [1,2,3,4]
=> "maybe"
Triple Equality
Similarly to ==
, ===
is also a method call. When we’re making this call we want to be really sure that the things are equal. Getting back false
would be disappointing, so we can override this method to always return true
:
class Array
def ===(_)
true
end
end
Not Equal
Just like its siblings ==
and ===
, !=
is also a method call. We can override this:
class Array
def !=(_)
"can't say, tbqh"
end
end
We can’t say, to be quite honest.
Unary Minus (again)
We saw an example of unary minus working on a variable, but unary methods can be called before data types in Ruby too. Strings are one case where we can freeze a string by prefixing it with -
:
>> a = -"string"
=> "string"
>> a.frozen?
=> true
But arrays are another case. Arrays in Ruby don’t have a unary minus method defined by default, but that doesn’t stop us defining our own:
class Array
def -@
clear
end
end
What this code allows us to do is to clear an array by prefixing it with -
. It’ll work for the array itself, or a variable representing the array too:
>> -[1,2,3]
=> []
>> a = [1,2,3]
=> [1, 2, 3]
>> -a
=> []
>> a
=> []
This saves us a full 5 characters of typing that we would otherwise have to do:
>> a.clear
Unary Plus
Just like -@
, we can add a +@
method to arrays:
def +@
replace flat_map { |x| [x] * 10 }
self
end
The Japanese “十” character is the one for 10, so it makes sorta-sense that our +@
method takes each of the element, and makes 10 of those in the array:
>> a = [1,2,3]
=> [1,2,3]
>> +a
=> [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
This operation mutates the array using replace
, replacing whatever’s in the array with 10 of each of the elements.
It should be noted that *
is also an Array method, but this one hasn’t been overriden here. I should also note that the *
method can take either an Integer or a String as an argument and it behaves differently depending. Check out the docs for more info.
Unary Bang
We’ve seen a few unary methods so far, but an even lesser known one is the !@
unary method. My override for this is straightforward:
class Array
def !@
map { |x| rand(100) }
end
end
This will give us random numbers in our array, between 0 and 100:
>> ![1,2,3]
=> [70, 11, 82]
Unary Tilde
The last in the long line of unary methods is ~@
. This one kinda looks like a wave, so I think it should shuffle arrays:
class Array
def ~@
shuffle
end
end
Here’s an example of using it:
>> ~[1,2,3]
=> [2, 1, 3]
This saves us a grand total of 8 characters. Wow, such savings!
Unary combos
As we saw before with the unary methods +@
and -@
, we can chain them:
>> --------a
=> 5
>> ++++++++a
=> 5
>> +-+-+a
=> 5
Same goes for these array methods:
>> !+~[1,2,3]
=> [53, 68, 83, 5, 66, 98, 55, 73, 0, 40, 93, 71, 83, 38, 78, 68, 11, 29, 83, 88, 86, 2, 8, 85, 72, 77, 50, 96, 78, 36]
Why would you want to do this? I am not sure. I think it is a quirk of the Ruby language that allows this.
But the order matters:
>> +!~[1,2,3]
Traceback (most recent call last):
1: from /usr/local/opt/asdf/installs/ruby/2.5.1/bin/irb:11:in `<main>'
SyntaxError ((irb):4: syntax error, unexpected !~)
+!~[1,2,3]
^~
Typing without typing
That covers all my Ruby tricks in the video (and some more!), but there were a couple of other tricks I should mention. There were two distinct tricks: one where the code typed itself, and another where a terminal displayed a card after someone spoke it.
Self-writing code
It sure would be nice if code wrote itself. But alas, technology hasn’t reached that particular zenith yet.
We can simulate this sort of technology using other tech, such as Asciinema. This will record your terminal session, and you can play it back as you wish. This is what I did when I wrote the original cards.rb
.
ActiveListening
The second cards.rb
, works with a dual keypress on the keyboard. Six of diamonds is “6D”. Ten of hearts is “0H” or “TH”. Jokers aren’t used in any serious card games, so they are not accounted for in this script.
So this code, if you read it, works by taking terminal input of two characters. But during the talk I get Kasia to read out some cards and then, a little while later, they appear on the screen.
But how?
This is a cheeky trick, and I needed another assistant for it. I recruited one of my juniors, Nick Wolf for this. I ran a tmux
session for all the code demos during the talk, and gave Nick ssh
access to my machine.
Nick then connected in via ssh
, ran tmux attach-session
and then could control my terminal as easily as I could. When Kasia read out a card, Nick would type in the two characters required for that card.
Magic isn’t magic if you know how it works. This trick was a little cheeky, but I included it as I wanted to show off that tmux
can allow two people to share a terminal over SSH.
Conclusion
I hope this post has been helpful to understand what strange things Ruby is capable of. There’s no logical reason to override the +
method on Integer
, or to add extra unary methods to Arrays. It’s just something that Ruby allows us to do because of the language’s flexibility.
Other people have done truly amazing things with the Ruby language. There’s the trick2018 repo which includes some really amazing Ruby files. Go through those and take a look.
My favourite though is the qlobe – a quine that outputs a rotating globe of the earth. It even remembered to include New Zealand.