Ruby has become my programming language of choice ever since a software engineering course introduced to the langauge in univeristy more than 4 years ago.
During my time working with Ruby, I’ve encountered plenty of interesting functionality.
This blog post is my attempt to share my experience with some of Ruby’s more uncommon features.
1. Module#prepend
It’s a common idiom in Ruby to mix in additional behaviour to a class via modules:
module Cooking
def cook
'sizzle sizzle'
end
end
class Bacon
include Cooking
end
Bacon.new.cook # => "sizzle sizzle"
This pattern works well to share similar behaviours between classes and avoid complexity that arises from inheritance.
Sometimes we want to be able to redefine a method on a class from a module. The Module#prepend method will let us do just that:
module Cooking
def cook
'sizzle sizzle'
end
end
class Bacon
prepend Cooking
def cook
'crackle crackle'
end
end
Bacon.new.cook # => "sizzle sizzle"
This pattern will also allow the use of super
in the method override to access the original definition:
module Cooking
def cook
"sizzle sizzle #{super}"
end
end
class Bacon
prepend Cooking
def cook
'crackle crackle'
end
end
Bacon.new.cook # => "sizzle sizzle crackle crackle"
2. RubyVM::InstructionSequence#disasm
The Ruby standard library provides the RubyVM::InstructionSequence class to give developers access to the interpreter’s internals.
We can actually decompile the internal YARV bytecode that is generated for a block of code:
code = <<-RUBY
puts rand(3)
RUBY
puts RubyVM::InstructionSequence.new(code).disasm
# OUTPUT
# == disasm: #<ISeq:<compiled>@<compiled>>================================
# 0000 trace 1 ( 1)
# 0002 putself
# 0003 putself
# 0004 putobject 3
# 0006 opt_send_without_block <callinfo!mid:rand, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
# 0009 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
# 0012 leave
For most developers, this will be more of an interesting feature as opposed to useful one. Generally, I have only disassembled Ruby code to learn more about how the interpreter works.
3. Overriding the Backtick Method
It’s possible to override the Kernel::` method in the standard library.
Normally, this method is used to execute shell commands directly. But we can change the behaviour:
`echo $SHELL` # => "/bin/zsh\n"
def `(command)
puts "command: #{command}"
end
`echo $SHELL` # => "command: echo $SHELL"
A use case of this may be to mock system commands in tests, or to log a command before it is executed. With that being said, most Ruby developers agree that it’s probably best to not use backticks at all.
4. IRB-fu
I use irb
all the time to quickly execute code and perform quick debugging. Many developers aren’t aware that irb
comes with its own bag of tricks:
- The
_
variable is always the last value that the interpreter evaluated.
2.times.map { rand(10) }
# oops, I forgot to save the result!
result = _ # => [2, 8]
-
Run
irb
with warnings enabled.The
-w
flag will enable the interpreter’s warning generation.Just add
alias irb="irb -w
to your shell’s initialization script to permanently enable warnings.Here’s an example of an interpreter warning you might see:
File.exists?('myfile.txt')
(irb):1: warning: File.exists? is a deprecated name, use File.exist? instead
# => false
5. The caller
Method
Have you ever started debugging an issue and then quickly wondered how a method was actually called?
That’s exactly what Ruby’s built-in caller
method is used for.
The caller
method returns an array that contains the current stack trace and really helps the developer follow complicated program flow.
# in file test.rb
def show_caller
puts caller
end
show_caller
# OUTPUT
# > ruby test.rb
# test.rb:5:in `<main>'
The caller
method really shines when combined with a debugger like pry to dynamically halt the program.
6. The retry
Keyword
Ruby gives us a retry
keyword that simply re-executes a block code until specified:
begin
num = rand(10)
puts "Attempt: #{num}"
raise StandardError
rescue StandardError
retry unless num == 1
puts "Success!"
end
# OUTPUT
# Attempt: 5
# Attempt: 9
# Attempt: 9
# Attempt: 3
# Attempt: 2
# Attempt: 6
# Attempt: 4
# Attempt: 1
# Success!
The retry
keyword can be useful for actions that may be unreliable (i.e. flaky API endpoints).
7. The method
Method
Ruby’s method()
method is probably my most-used hidden gem in the standard library.
At any point in execution, we can create an instance of a method at runtime. This instance stores values that are very useful for debugging such as the source location.
Here’s an example to show where Rails.configuration
is defined:
require 'rails'
method = Rails.method(:configuration)
method.source_location # => ["/Users/jesse/.rvm/gems/ruby-2.3.0/gems/railties-5.0.0/lib/rails.rb", 43]
If you have the method_source gem installed on your system, then you can also view the raw source code of the method:
require 'method_source'
require 'rails'
method = Rails.method(:configuration)
puts method.source
# OUTPUT
# def configuration
# application.config
# end
Finally, it’s possible to use method
somewhat like Symbol#to_proc:
['/etc/hosts', '_missing_'].map(&File.method(:exist?)) # => [true, false]
I’m sure that this list just scratches the surface for Ruby’s hidden functionality. Feel free to let me know if you think there is something missing from this list!