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