DSL building patterns in Ruby - Time & Space Complexity
When creating a DSL (Domain Specific Language) in Ruby, we want to know how the time it takes to run grows as we add more commands or rules.
We ask: How does the execution time change when the DSL script gets longer or more complex?
Analyze the time complexity of the following Ruby DSL builder code.
class SimpleDSL
def initialize
@commands = []
end
def command(name, &block)
@commands << {name: name, action: block}
end
def run
@commands.each { |cmd| cmd[:action].call }
end
end
# Usage
# dsl = SimpleDSL.new
# dsl.command(:say_hello) { puts 'Hello' }
# dsl.command(:say_goodbye) { puts 'Goodbye' }
# dsl.run
This code collects commands in a list and runs each command's action one by one.
Look for loops or repeated steps in the code.
- Primary operation: Looping through all stored commands in
runmethod. - How many times: Once for each command added to the DSL.
As you add more commands, the run method calls each command's action one time.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | 10 calls to command actions |
| 100 | 100 calls to command actions |
| 1000 | 1000 calls to command actions |
Pattern observation: The number of operations grows directly with the number of commands added.
Time Complexity: O(n)
This means the time to run the DSL grows in a straight line as you add more commands.
[X] Wrong: "Adding more commands won't affect run time much because each command is simple."
[OK] Correct: Even if each command is simple, running all commands means doing more work overall, so time grows with the number of commands.
Understanding how your DSL runs helps you write efficient code and explain your design choices clearly in interviews.
"What if the DSL stored commands in a nested structure and ran them recursively? How would the time complexity change?"
Practice
Solution
Step 1: Understand DSL purpose
A DSL is designed to simplify and clarify code for a specific domain or task.Step 2: Compare options
Only To make code easier to read and write for specific tasks correctly states the purpose of DSLs; others describe unrelated goals.Final Answer:
To make code easier to read and write for specific tasks -> Option AQuick Check:
DSL purpose = simplify domain code [OK]
- Thinking DSLs speed up Ruby interpreter
- Believing DSLs replace Ruby syntax entirely
- Confusing DSLs with UI generators
Solution
Step 1: Recall Ruby block execution methods
Ruby provides methods like instance_eval and class_eval to run blocks in specific contexts.Step 2: Identify the method for DSL context
instance_eval runs the block inside the instance's context, which is typical for DSLs.Final Answer:
instance_eval -> Option DQuick Check:
Block runs in object context = instance_eval [OK]
- Confusing class_eval with instance_eval
- Using non-existent methods like eval_block
- Assuming block_exec is a Ruby method
class Builder
def initialize
@steps = []
end
def step(name)
@steps << name
end
def build(&block)
instance_eval(&block)
@steps
end
end
b = Builder.new
result = b.build do
step 'clean'
step 'compile'
step 'test'
end
puts result.inspectSolution
Step 1: Understand the Builder class behavior
The build method runs the block inside the Builder instance, collecting step names as strings in @steps.Step 2: Trace the block execution
Each call to step adds the string name to @steps: 'clean', 'compile', 'test'. The build method returns @steps.Final Answer:
["clean", "compile", "test"] -> Option BQuick Check:
Collected steps = ["clean", "compile", "test"] [OK]
- Expecting symbols instead of strings
- Thinking step method returns step name
- Confusing array contents with method names
class Configurator
def set(name, value)
@settings ||= {}
@settings[name] = value
end
def configure(&block)
instance_eval(block)
end
end
c = Configurator.new
c.configure do
set :mode, 'fast'
endSolution
Step 1: Check instance_eval usage
instance_eval expects a block passed with &block, but here it is called as instance_eval(block) without &.Step 2: Understand correct block passing
The correct call is instance_eval(&block) to convert the block argument to a block for instance_eval.Final Answer:
Wrong argument passing to instance_eval -> Option AQuick Check:
Use &block with instance_eval [OK]
- Calling instance_eval without & for block
- Forgetting to initialize @settings
- Expecting set to return a value
config = Config.new do
database do
adapter 'sqlite3'
pool 5
end
cache do
enabled true
size 64
end
end
Which approach best supports this nested DSL pattern?Solution
Step 1: Understand nested DSL needs
Nested blocks require separate context objects for database and cache to hold their settings.Step 2: Identify best practice
Using instance_eval on separate classes for each nested block cleanly isolates configuration and supports nested DSL.Final Answer:
Use instance_eval with separate classes for database and cache blocks -> Option CQuick Check:
Nested DSL = instance_eval + separate classes [OK]
- Using global variables causing conflicts
- Using class variables mixing state
- Using eval with strings risking security and complexity
Ready to prove it?
Pass the test and this pattern moves unknown
