📖 Scenario: Imagine you want to create a small language inside Ruby to describe a recipe. This language will let you write recipes in a way that looks like plain English but runs as Ruby code.
🎯 Goal: You will build a simple Domain Specific Language (DSL) in Ruby to describe a recipe with ingredients and steps. You will then print the recipe in a readable format.
📋 What You'll Learn
Create a class called Recipe to hold ingredients and steps
Add a method called ingredient to add ingredients with name and quantity
Add a method called step to add instructions
Use a block to define the recipe content
Print the recipe with ingredients and steps in order
💡 Why This Matters
🌍 Real World
DSLs help make complex tasks easier by creating small languages tailored to specific problems, like writing recipes, configuring apps, or describing workflows.
💼 Career
Understanding DSL building patterns is useful for Ruby developers working on frameworks, testing tools, or configuration systems where readable and flexible code is important.
Progress0 / 4 steps
1
Create the Recipe class with storage
Create a class called Recipe with two instance variables: @ingredients and @steps. Initialize both as empty arrays inside the initialize method.
Ruby
Hint
Use @ingredients = [] and @steps = [] inside the initialize method.
2
Add methods to add ingredients and steps
Add two methods inside the Recipe class: ingredient(name, quantity) which adds a hash with keys :name and :quantity to @ingredients, and step(description) which adds the description string to @steps.
Ruby
Hint
Use @ingredients << { name: name, quantity: quantity } and @steps << description inside the methods.
3
Add a class method to build the recipe with a block
Add a class method self.build that takes a block. Inside it, create a new Recipe instance, yield it to the block, and return the instance.
Ruby
Hint
Use yield(recipe) to run the block with the recipe object.
4
Print the recipe ingredients and steps
Add a method print_recipe inside the Recipe class that prints "Ingredients:" followed by each ingredient's quantity and name on separate lines, then prints "Steps:" followed by each step numbered starting from 1. Then create a recipe using Recipe.build with these exact ingredients and steps: ingredient("Flour", "2 cups"), ingredient("Sugar", "1 cup"), step("Mix ingredients"), step("Bake for 30 minutes"). Finally, call print_recipe on the created recipe.
Ruby
Hint
Use puts to print lines. Use each to loop through ingredients and steps. Use each_with_index to number steps.
Practice
(1/5)
1. What is the main purpose of using a DSL (Domain Specific Language) in Ruby?
easy
A. To make code easier to read and write for specific tasks
B. To speed up Ruby interpreter execution
C. To replace Ruby's core syntax completely
D. To automatically generate user interfaces
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 A
Quick Check:
DSL purpose = simplify domain code [OK]
Hint: DSLs focus on specific tasks for clearer code [OK]
Common Mistakes:
Thinking DSLs speed up Ruby interpreter
Believing DSLs replace Ruby syntax entirely
Confusing DSLs with UI generators
2. Which Ruby method is commonly used to execute a block inside the context of a DSL object?
easy
A. eval_block
B. block_exec
C. class_eval
D. instance_eval
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 D
Quick Check:
Block runs in object context = instance_eval [OK]
Hint: Use instance_eval to run blocks inside DSL objects [OK]
Common Mistakes:
Confusing class_eval with instance_eval
Using non-existent methods like eval_block
Assuming block_exec is a Ruby method
3. What will be the output of this Ruby DSL code?
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.inspect
medium
A. [:clean, :compile, :test]
B. ["clean", "compile", "test"]
C. ["step", "step", "step"]
D. SyntaxError
Solution
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.
Hint: instance_eval runs block adding strings to array [OK]
Common Mistakes:
Expecting symbols instead of strings
Thinking step method returns step name
Confusing array contents with method names
4. Identify the error in this DSL builder code:
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'
end
medium
A. Wrong argument passing to instance_eval
B. Missing @settings initialization
C. set method missing return value
D. Block not passed to configure method
Solution
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 A
Quick Check:
Use &block with instance_eval [OK]
Hint: Pass block with & when calling instance_eval [OK]
Common Mistakes:
Calling instance_eval without & for block
Forgetting to initialize @settings
Expecting set to return a value
5. You want to build a Ruby DSL that allows nested configuration like this:
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?
hard
A. Use global variables to store settings inside blocks
B. Use class variables to share state across blocks
C. Use instance_eval with separate classes for database and cache blocks
D. Use eval with string interpolation for nested blocks
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 C
Quick Check:
Nested DSL = instance_eval + separate classes [OK]
Hint: Use instance_eval with helper classes for nested blocks [OK]
Common Mistakes:
Using global variables causing conflicts
Using class variables mixing state
Using eval with strings risking security and complexity
Recommended
Ready to prove it?
Pass the test and this pattern moves unknownstrongwhile earning +1 Grip toward your certificate.