There's More to Ruby Debugging Than puts()
Share
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." - Brian W. Kernighan
Debugging is always challenging, and as programmers we can easily spend a good chunk of every day just trying to figure out what is going on with our code. Where exactly has a method been overwritten or defined in the first place? What does the inheritance chain look like for this object? Which methods are available to call from this context?
This article will take you through some under-utilized convenience methods in Ruby which will make answering these questions a little easier.
#class
Calling #class
on an instance will reveal what type of Object it is. This can be useful for verifying what classes you're dealing with, and if an instance has decided it's OK to masquerade as another class.
require 'forwardable'
class OptionsHash
attr_reader :hash
extend Forwardable
def_delegators :@hash, *(Hash.instance_methods - Object.instance_methods)
def initialize(*args)
@hash = args.first.is_a?(Hash) ? args.first : Hash.new(*args)
end
def ==(other)
@hash == other
end
end
real_hash = Hash.new
my_hash = OptionsHash.new(real_hash)
puts my_hash == real_hash #=> true
puts my_hash.class == real_hash.class #=> false
#is_a?
, #kind_of?
These methods allow you to verify if an instance is a specific class, one of its superclasses, or if its a module thats been included by the class of that instance.
#instance_of?
#instance_of
verifies if an instance is a specific class.
#methods
, #public_methods
, #protected_methods
, #private_methods
, #singleton_methods
#methods
returns a list of all of the public and protected methods on an instance. This includes all methods defined directly on its class, or by those of its ancestors or modules. #public_methods
, #protected_methods
, #private_methods
, and #singleton_methods
limit the returned list of methods to their specific scope.
A word of caution: these methods will return all of the methods that have been inherited by the instance. You'll almost always want to filter them by subtracting the corresponding method types from Object
or another base ancestor, such as ActiveRecord::Base
in Rails.
#method
#method
is very useful. It enables us to inspect a method's signature, source location and many other details. This can help with:
- Narrowing down precisely where a method has been overridden (
#source_location
) - Casting a method to a Proc for use elsewhere (
#to_proc
) - Determining how many arguments a method takes (
#arity
), and what they're named (#parameters
) - In the case of aliased methods (
#original_name
)
#caller
#caller
is extremely useful. It allows you to look at exactly what the chain of methods was that called your current method, and exactly what files to look in to see them.
#instance_variables
, #instance_variable_get
, #instance_variable_set
, #instance_variable_defined?
, #remove_instance_variable
#instance_variables
allows us to peek into an instance and reveal information about its current state. #instance_variable_get
and #instance_variable_set
provide an external way to manipulate an object's internal state.
Working with Classes
Classes are Objects, just like everything else in Ruby. This means we can define, and inspect them in similar ways. In addition to the previous section there are a few additional methods available to us. Try to keep in mind that a specific class is just a descendant of the Class
object.
::name
::name
returns the name of the class in String
form.
::class_variables
, ::class_variable_get
, ::class_variable_set
, ::class_variable_defined?
, ::remove_class_variable
Similar to the #instance_variables
methods, these methods allow us to see what variables have been defined specifically on this class as well as manipulate those class variables.
::instance_methods
, ::instance_method
::instance_methods
will return a list of methods that will be available on an instance of the class. ::instance_method
is almost the same as its #method
counterpart. The difference is that it returns an unbound method, which means it does not have a receiver and is not actually callable yet.
::included_modules
This provides a list of the modules that have been included in the class already.
::superclass
This provides the immediate parent for the class.
Working with Modules
Modules add another layer of flexibility for providing reusable components shared by many classes. There are some specific methods available which can help when debugging.
Module.nesting
This provides a list of modules, in order, that encapsulate the current module. This is useful for figuring out Class
and Module
naming conflicts, as well as the order that Classes will be looked up. This method cannot be evaluated externally. It has to be evaluated from within the context you wish to inspect.
::ancestors
This provides a list of all modules included in the class, in order of the inheritence chain.
::constants
, ::const_get
, ::const_set
, ::const_defined?
These are the same as the similarly named #instance_variables
, #instance_variable_get
, and #instance_variable_set
methods, but for constants.
Working with Debuggers
Debuggers can provide a lot of very powerful tools for inspecting running Ruby code. Debugging code without a proper interactive debugger is the worst experience you can ever have. A good knowledge of the debugger can be all the difference in solving a problem in a few minutes, rather than hours or even days.
The basic debugger in Ruby provides a simple IRB interface for interacting with code live. For Ruby before 2.0 you'll want to add gem "debugger"
to your Gemfile. For Ruby 2.0 and newer you'll want to add gem "byebug"
.
The following section will explain a few of the most useful methods available to you while in the debuggers mentioned above.
var
var
allows us to inspect the current scope of variables. You can pass in all
, local
, global
, or instance
, and it will return the names and values of the variables that have been defined.
backtrace
Very similar to #caller
, backtrace
provides the current list of available frames and their locations.
frame
frame
provides your current location in the backtrace
. frame
also accepts an integer as argument and will move the debugger to that part of the backtrace
, allowing you to inspect the scope higher up the stack.
help
help
will list out your available commands. Use help <method>
to get information for a specific method.
Closing Remarks
Ruby is amazingly powerful, and provides awesome tools to allow you to building amazing things. The amount of flexibility provided to developers has spawned a vast ecosystem of invaluable code. It has also, on occasion, created situations where debugging can be difficult.
With the tools above you should be much better equipped to debug these situations. Of particular interest is the frame navigation in debuggers, since navigating to a frame allows you to inspect the state it was in at the time it was evaluated. This can be a great ally in detecting the location where a change was introduced in the call stack, enabling you to inspect the offending code.
View original comments on this article