Tracing global variables in Ruby using trace_var
Recently, I came across this “interesting” approach to printing the “99 bottles of beer” song on Rosetta code.
trace_var :$bottle_num do |val|
$bottles = %Q{#{val == 0 ? 'No more' : val.to_s} bottle#{val == 1 ? '' : 's'}}
end
($bottle_num = 99).times do
puts "#{$bottles} of beer on the wall"
puts "#{$bottles} of beer"
puts "Take one down, pass it around"
$bottle_num -= 1
puts "#{$bottles} of beer on the wall"
puts ""
endWait… WHAT!?
This is unlike any Ruby code I’ve seen before.
What’s trace_var?
What are those global variables doing?
Let’s start by looking at this trace_var thing.
The Kernel#trace_var
documentation says:
Controls tracing of assignments to global variables.
… the block is executed whenever the variable is assigned. The block or Proc object receives the variable’s new value as a parameter.
Let’s try something simple with this.
trace_var :$foo do |x|
puts "$foo = #{x}"
end
(1..5).each { |i| $foo = i }
# This prints:
# $foo = 1
# $foo = 2
# $foo = 3
# $foo = 4
# $foo = 5If you pass in a string instead of a block or Proc, it gets eval’ed.
trace_var :$foo, "puts 'changing foo'"
$foo = 2
# changing fooIf you need to unset tracing with trace_var,
you can do untrace_var :$foo.
Now that we’ve seen how trace_var works,
let’s go back to the “99 bottles” program.
It’s starting to make sense now.
trace_var :$bottle_num do |val|
$bottles = %Q{#{val == 0 ? 'No more' : val.to_s} bottle#{val == 1 ? '' : 's'}}
endThis assigns the string
"#{$bottle_num} bottles"
to the $bottles variable,
except when $bottle_num is zero,
in which case “No more bottles”
is assigned.
So each time $bottle_num gets changed,
the $bottles string gets updated automatically.
Debugging with trace_var
trace_var can be a useful debugging tool.
Suppose you want to find out
which line of code is
changing a global variable.
You could do this:
trace_var :$foo do |x|
puts "$foo = #{x}"
puts caller
endThis will show you assigned value and the execution stack at that point.
$foo = 42
foo.rb:27:in `call'
foo.rb:27:in `initialize'
foo.rb:31:in `new'
foo.rb:31:in `<main>'But if you ever find yourself debugging your code like this, maybe it’s a good time to get rid of those global variables. They are bad for your codebase.
trace_var only works with global variables.
If you need a more flexible tool
you might want to look at Ruby 2.0’s new
TracePoint API.
More weird Ruby
If you found this post interesting, you might also enjoy my other articles about lesser known features of Ruby, such as the flip flop operator, tail call optimization, memoization using metaprogramming tricks, or using Ruby as an alternative to sed/awk.