पाठ 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.