How to Create Sentence-Like DSLs in Ruby: Syntax Rules and Workarounds
Understanding Ruby's Grammar: Why Pure Sentence Syntax Fails
Ruby is famous for its expressive syntax and capability for creating Domain-Specific Languages (DSLs). However, trying to write a sentence like girl "Alice" with age 20 go to :home without any commas, dots, or operators triggers a Ruby syntax error (unexpected tIDENTIFIER).
This happens because of how Ruby's parser processes expressions:
- Method Calls: Ruby allows omitting parentheses on method calls (e.g.,
girl "Alice"), but consecutive expressions require a explicit delimiter (like a comma or semicolon) or an operator between them. - Receiver and Methods: To call a method on an object without an operator, Ruby requires a dot (
.), as inobject.method.
While you cannot eliminate delimiters entirely in standard Ruby without external code preprocessing, you can achieve near-sentence syntax using idiomatic Ruby DSL design patterns.
Approach 1: Method Chaining (The Fluent Interface)
The closest standard syntax to what you attempted uses method chaining. By returning self from intermediate methods, you can chain words together sequentially.
class PersonDSL
attr_reader :name, :person_age, :destination
def initialize(name)
@name = name
end
def with
self
end
def age(value)
@person_age = value
self
end
def go
self
end
def to(location)
@destination = location
self
end
end
def girl(name)
PersonDSL.new(name)
end
# Usage:
action = girl("Alice").with.age(20).go.to(:home)
puts "#{action.name}, age #{action.person_age}, is going to #{action.destination}"
Approach 2: Operator Overloading
If your goal is to avoid dots (.), you can overload operators such as +, >>, or / to act as glue between sentence components.
class SentenceBuilder
attr_reader :data
def initialize(initial_val)
@data = [initial_val]
end
def >>(other)
@data << other
self
end
end
def girl(name)
SentenceBuilder.new(name)
end
def with(val)
{ with: val }
end
def age(val)
{ age: val }
end
# Usage using the >> operator as a separator:
girl "Alice" >> with(age(20)) >> :home
Approach 3: Clean Block-Based DSL (The Idiomatic Ruby Way)
In production Ruby code (such as Rails or RSpec), sentence-like structures are typically handled using blocks and instance_eval. This removes the need for dots between chained calls while remaining valid Ruby syntax.
class GirlContext
attr_reader :name, :age_val, :location
def initialize(name, &block)
@name = name
instance_eval(&block) if block_given?
end
def with_age(number)
@age_val = number
end
def go_to(destination)
@location = destination
end
end
def girl(name, &block)
GirlContext.new(name, &block)
end
# Usage:
subject = girl "Alice" do
with_age 20
go_to :home
end
puts "#{subject.name} (age #{subject.age_val}) -> #{subject.location}"
Summary
Due to Ruby's grammar, a completely separator-free syntax like girl "Alice" with age 20 go to :home is impossible without a custom pre-parser. For real-world Ruby projects, standard method chaining or block-based DSLs provide the best balance of readability and syntax validity.