Ruby Prepend is super cool
What it is and how to use it wrong, maybe.
The use case
I was building out a deploy script that ran a bunch of checks.
class CICheck < Check def initialize puts "Checking CI" end end class SensitiveFilesCheck < Check def initialize puts "Checking sensitive files" end end
puts got annoying. What if I wanted to change that across all checks? Well, I could do something like this:
class Check def initialize(*) puts @description end end class CICheck < Check def initialize(sha) @description = "Checking CI" super @sha = sha end end
But that’s not really great. I have to remember to
super in every
Check. Feels like writing class components in React.
I wanted something cleaner, like:
class CICheck < Check desc "Checking CI" end
And have that do all the work.
Where there’s a will, there’s probably a Ruby method or keyword
I thought to myself: “there has to be a Ruby way to do this.” And with Matz as my witness, there is.
From the docs
According to RubyDoc[^1]:
Invokes Module.prepend_features on each parameter in reverse order.
Okay but what does that mean?
When this module is prepended in another, Ruby calls prepend_features in this module, passing it the receiving module in mod. Ruby’s default implementation is to overlay the constants, methods, and module variables of this module to mod if this module has not already been added to mod or one of its ancestors. See also Module#prepend.
Prepend to the rescue
Let’s get weird.
class Check def self.inherited(klass) klass.extend ClassMethods klass.prepend InstanceMethods end module ClassMethods def description @description end def desc(value) @description = value end end module InstanceMethods def initialize(*) puts self.class.description super end end end
You know where this is going:
class CICheck < Check desc "Checking CI" attr_reader :sha def initialize(sha) @sha = sha end end
And now I can safely:
CICheck.new("1234") # "Checking CI"