define_method and small DSLs
define_method
define_method creates a real method from a block. Unlike method_missing, the method exists: methods lists it, respond_to? is true, and the editor can find it:
class A
define_method(:x) { 1 }
end
p A.new.x
p A.instance_methods(false)
ruby -e 'class A; define_method(:x) { 1 }; end; p A.new.x; p A.instance_methods(false)'
1 [:x]
The block is a closure, so it sees the variables around the definition. That is what lets a loop define a family of methods, each with its own value captured.
instance_variable_set
A method created from a name needs storage named from the same name. instance_variable_set and instance_variable_get take the @ name as a symbol or a string:
define_method("#{name}=") { |value| instance_variable_set("@#{name}", value) }
Reading uses instance_variable_get("@#{name}"). The pair is a hash with one slot per name, written where a reader of the class can see the name. A method that builds the name from user input is a code injection; build it from a list.
A class-level DSL
A class macro is a class method that defines instance methods. The setting method below is the whole pattern:
class Settings
def self.setting(name, default:)
define_method(name) { instance_variable_get("@#{name}") || default }
define_method("#{name}=") { |value| instance_variable_set("@#{name}", value) }
end
setting :host, default: "localhost"
setting :port, default: 3000
end
Two calls, four methods, and the defaults live next to the names. That is the DSL a configuration class, a serialiser or a test factory uses before it needs a framework.
Running it
ruby dsl.rb
"localhost" 4000 [:host, :host=, :port, :port=]
The default answers until a writer sets a value; the writer returns the value it stored, which is Ruby's assignment rule, not a choice. The list of instance methods is the proof that the names are real.
What to watch for
A DSL that reads as English is easy to write and hard to debug. Keep the
macros few, give them a plain method underneath, and let the macro call
that method; a stack trace that names
define_method for every frame is a day lost.
define_method is slower to call than a
def in old Rubies and about the same in modern
ones. The reason to choose it is that the name is data, not speed.
Next: Refinements
Article author: Arthur Isaev