Ruby debugging for the curious ones
Recently I’ve been navigating through some Ruby on Rails systems that are new to me. There are many ways to understand code, and I’ve found that a great way to understand how the code works, is to explore the code.
Some useful gems for deep diving are:
gem "pry"
gem "pry-rails"
gem "pry-stack_explorer"
gem "pry-byebug"
gem "byebug"
We can also utilize the excellent binding.irb
, which is built in to Ruby since 2.4.0
but it can be cumbersome not having some easy helpers like ls
, step
, or continue
. Let’s check out the others.
How do we utilize these? The simplest way is to start with binding.pry
.
binding.pry
, byebug
and stack explorer
allow us to navigate throughout our Rails program at every frame. We can utilize the tools these gems give us to visualize quite a bit. Some of the helpful commands we can use are available here
Let’s create a class that transforms objects given to it into fun output. We’ll use this class to see how we can understand the methods available on an object and in the console itself.
require "pry"
class AbstractInfoPrinter; end
class SportsInfoPrinter < AbstractInfoPrinter
SPORTS = %w[⚽️ 🏀 🏈 ⚾️]
attr_reader :object_to_print
def initialize(obj)
@object_to_print = obj
end
def print
transformed_object = transform_object
puts transformed_object
end
def transform_object
str = ""
object_to_print.map do |key, value|
str << "Player number #{key} has #{value} #{sports_sample}'s"
end
end
private
def sports_sample
SPORTS.sample
end
end
IRB helpers
We can visualize more of what this class does by creating a new instance.
[2] pry(main)> SportsInfoPrinter.methods
=> [:allocate, :superclass, :new, :<=>, :<=, :>=, :==, :===, :included_modules, :include?, :name, :ancestors, :attr, :attr_reader, :attr_writer, :attr_accessor, :instance_methods, :public_instance_methods, :protected_instance_methods, :private_instance_methods, :constants, :const_get, :const_set, :const_defined?, :class_variables, :remove_class_variable, :class_variable_get, :class_variable_set, :class_variable_defined?, :freeze, :inspect, :private_constant, :public_constant, :const_missing, :deprecate_constant, :include, :singleton_class?, :prepend, :module_exec, :module_eval, :class_eval, :remove_method, :<, :>, :undef_method, :class_exec, :method_defined?, :alias_method, :to_s, :private_class_method, :public_method_defined?, :private_method_defined?, :protected_method_defined?, :public_class_method, :autoload, :autoload?, :instance_method, :public_instance_method, :define_method, :hi_there, :instance_variable_defined?, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :instance_variable_set, :protected_methods, :instance_variables, :instance_variable_get, :public_methods, :private_methods, :method, :public_method, :public_send, :singleton_method, :define_singleton_method, :extend, :to_enum, :enum_for, :=~, :!~, :eql?, :respond_to?, :object_id, :send, :display, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :yield_self, :then, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :frozen?, :methods, :singleton_methods, :equal?, :!, :instance_exec, :!=, :instance_eval, :__id__, :__send__]
Look at all those methods available, 112! Let’s try a few out.
[3] pry(main)> SportsInfoPrinter.included_modules
=> [PP::ObjectMixin, Kernel]
Included modules can help use know if we’ve included something we shouldn’t have, whether through explicit inclusion in the class itself, or through inheritance.
[4] pry(main)> SportsInfoPrinter.superclass
=> AbstractInfoPrinter
[5] pry(main)> SportsInfoPrinter.superclass.superclass
=> Object
We can even chain these methods, to understand the inheritance better.
Debugging
Let’s add binding.pry
into one of the methods of our class, re-define the class in the console, then test it out.
...
def transform_object
str = ""
object_to_print.map do |key, value|
binding.pry
str << "Player number #{key} has #{value} #{sports_sample}'s"
end
end
...
[2] pry(main)> ip = SportsInfoPrinter.new({five: 5})
=> #<SportsInfoPrinter:0x00007ff67617d6c0 @object_to_print={:five=>5}>
[3] pry(main)> ip.print
From: (pry):16 SportsInfoPrinter#transform_object:
12: def transform_object
13: str = ""
14: object_to_print.map do |key, value|
15: binding.pry
=> 16: str << "Player number #{key} has #{value} #{sports_sample}'s"
17: end
18: end
[1] pry(#<SportsInfoPrinter>)>
binding.pry
has pulled up a new instance of pry for us to work with. We can step around the code, evaluate certain things, and see the value of variables. Let’s run a bunch of commands to see what’s available to us
[1] pry(#<SportsInfoPrinter>)> key
=> :five
[2] pry(#<SportsInfoPrinter>)> value
=> 5
[3] pry(#<SportsInfoPrinter>)> sports_sample
=> "⚾️"
[4] pry(#<SportsInfoPrinter>)> sports_sample
=> "🏈"
[5] pry(#<SportsInfoPrinter>)> key = :nine
=> :nine
[6] pry(#<SportsInfoPrinter>)> value
=> 5
[7] pry(#<SportsInfoPrinter>)> ls
SportsInfoPrinter#methods: object_to_print print transform_object
instance variables: @object_to_print
locals: _ __ _dir_ _ex_ _file_ _in_ _out_ key pry_instance str value
[8] pry(#<SportsInfoPrinter>)> whereami
From: (pry):16 SportsInfoPrinter#transform_object:
12: def transform_object
13: str = ""
14: object_to_print.map do |key, value|
15: binding.pry
=> 16: str << "Player number #{key} has #{value} #{sports_sample}'s"
17: end
18: end
[9] pry(#<SportsInfoPrinter>)> __callee__
=> :transform_object
[10] pry(#<SportsInfoPrinter>)> __method__
=> :transform_object
[11] pry(#<SportsInfoPrinter>)> step
From: (pry):21 SportsInfoPrinter#sports_sample:
20: def sports_sample
=> 21: SPORTS.sample
22: end
[11] pry(#<SportsInfoPrinter>)> next
From: (pry):10 SportsInfoPrinter#print:
8: def print
9: transformed_object = transform_object
=> 10: puts transformed_object
11: end
[11] pry(#<SportsInfoPrinter>)> continue
Player number nine has 5 🏈's
=> nil
As you can see, there are some great tools available to us!