पाठ 15 / 25
Blocks and DSLs
Understand how Ruby DSLs such as RSpec, Rake and Rails routes work.
Configuration that reads like language
Ruby's flexible syntax (optional parentheses, blocks, keyword arguments, symbols) makes it ideal for internal domain-specific languages (DSLs). Familiar examples: RSpec (describe ... it ... expect(x).to eq(y)), Rake tasks, Rails routes (resources :orders), Bundler's Gemfile, Sinatra routes and many configuration files. Most DSLs combine blocks with instance_eval or instance_exec, which run a block with self set to a builder object, so bare method calls inside the block go to the builder. Alternatively, a DSL can yield a configuration object (configure do |config| config.timeout = 5 end), which keeps self unchanged and is easier to debug. Good DSLs are small, documented and validate their input; they make configuration and specifications readable for domain experts. The trade-off is indirection: when something goes wrong inside instance_eval, error messages and method lookup can be confusing, so prefer the yielding style unless the extra readability is worth it.
A tiny pricing DSL
instance_eval lets rule definitions read like a specification.
class PricingRules
Rule = Data.define(:name, :condition, :discount)
def self.define(&block)
rules = new
rules.instance_eval(&block) # bare calls inside the block go to rules
rules
end
def initialize = @rules = []
def discount(name, percent:, condition:)
@rules << Rule.new(name:, condition:, discount: percent)
end
def apply(order)
rule = @rules.find { |r| r.condition.call(order) }
rule ? order[:total] * (100 - rule.discount) / 100.0 : order[:total]
end
end
rules = PricingRules.define do
discount :festive, percent: 10, condition: ->(o) { o[:total] >= 2000 }
discount :student, percent: 5, condition: ->(o) { o[:student] }
end
puts rules.apply({ total: 2500 }) # 2250.0
puts rules.apply({ total: 1000, student: true }) # 950.0
# a yielding style is often easier to debug:
# MyApp.configure { |c| c.timeout = 5 }Avoid reserved words as keyword names
Ruby lets you declare a keyword argument named when: or if:, but you cannot then read it as a local variable without binding.local_variable_get. Choose plain names such as condition: for DSL options.
त्वरित जाँच: How do many Ruby DSLs make bare method calls inside a block reach a builder object?
- By using global variables
- By compiling the block to C
- By running the block with instance_eval or instance_exec so self is the builder
- By using threads
Answer
By running the block with instance_eval or instance_exec so self is the builder — instance_eval changes self for the block, routing implicit calls to the builder.