---
title: 'Domain-Specific Languages'
source: 'https://academia.sh/en/courses/object-orientation-and-metaprogramming/domain-specific-languages'
course: 'Object Orientation and Metaprogramming'
language: en
updated: '2026-10-21T05:03:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that a declaration method turns a single line into a method written by code'
    - 'Measures where a class method is looked up through the singleton class chain'
    - 'Explains how instance_eval changes self and hides outer instance variables'
    - 'Reads the authors of the whole course from the closing table'
---

# Domain-Specific Languages

Measuring which author hides behind a declaration line such as field :price, 0, where class methods are looked up, and where the block form breaks.

The previous lesson showed that a name can be **nobody** first and **code** afterward. This lesson uses the same transition on purpose: a single line such as `field :price, 0` looks like a call when it is read, but when it runs it adds a new method to the class. A **domain-specific language** is a form of writing that is reduced for a particular job and speaks with the words of that job. In Ruby one way to build it is declaration methods called in a class body. The shortness of the line does not make measuring it unnecessary; on the contrary, behind such lines it is not obvious at first glance who wrote the method.

The axis has been the same since the start of the course: **who wrote this method?** The author in this lesson is **code**. The programmer wrote the line `field :price, 0`, but the `price` method this line gave birth to is written by Ruby code, that is, by a `define_method` call. The measurements below draw this distinction; at the end, the place where the block form breaks is shown. The `author` helper is given in its form from the earlier lessons.

```ruby
# author.rb — a helper that classifies who wrote a method

# Classification asks in order: is there a definition in the receiver's chain at all,
# then does the receiver's own class carry it, then how was the definition produced.
def author(receiver, name)
  chain = receiver.singleton_class
  unless chain.method_defined?(name) || chain.private_method_defined?(name)
    return ["nobody", "no definition"]
  end

  definition = chain.instance_method(name)
  owner = definition.owner
  place = owner == chain ? "singleton class" : owner.to_s
  return ["ancestor", place] unless owner == receiver.class || owner == chain

  iseq = RubyVM::InstructionSequence.of(definition)
  return ["code", "#{place}, no ruby body"] if iseq.nil?
  return ["code", "#{place}, block body"] if iseq.label.start_with?("block")

  ["programmer", "#{place}, def body"]
end
```

This block is source; the file is written to disk and pulled in with `require_relative`.

## The Measured Class

The observation is on two classes. `Model` carries the declaration method; `Product` uses it.

```ruby
# model.rb — a base class that carries a declaration method
require_relative "author"

class Model
  def self.field(name, default)
    define_method(name) { instance_variable_get("@#{name}") || default }
  end
end

class Product < Model
  field :name, "unnamed"
  field :price, 0
end
```

This block is source too. `field` takes a name and a default value and defines a reader method for the name. The reader returns the instance variable if it is filled, and the default if it is empty. The two lines in the `Product` body are calls, not definitions: there is not a single `def` in the class.

## What the Declaration Line Wrote

The observation of the lesson is the `Product.new.price` message. First what the method returns is measured, then who wrote it and what appears in the class.

```ruby
# measurement.rb — the method the declaration line gave birth to
require_relative "model"

pr = Product.new
p pr.price
p pr.name
p author(pr, :price)
p Product.instance_methods(false).sort
```
```
0
"unnamed"
["code", "Product, block body"]
[:name, :price]
```

The method returned `0`, because the instance variable was empty and the default came into play. The author came out **code**: `author` found the definition in the link of the chain that belongs to `Product`, and the label of the instruction sequence pointed to a block body. The class's own method list is `[:name, :price]`; two declaration lines gave birth to two methods. The programmer wrote only two lines, but the interface of the class is two methods. **MP30.** This measurement is for a single declaration method and two fields; had `field` been written with another body, the author could have changed.

## A Call, but the Author Is Code

What the method really reads is the instance variable. When the variable is filled, the default should stay behind.

```ruby
# value.rb — the default and a filled value
require_relative "model"

pr = Product.new
pr.instance_variable_set(:@price, 40)
p pr.price
p Product.new.price
p author(Product, :field)
```
```
40
0
["ancestor", "#<Class:Model>"]
```

The filled variable beat the default: `40`. A new object of the same class gave `0`, because the default belongs to the class and the value belongs to the object. The last line separates the owner from the receiver: the author of the `field` method came out **ancestor**, and the place appeared as the singleton class of `Model`. The class `Product` was given as the receiver; the definition of `field` sits not in the own singleton class of `Product` but in that of `Model`. Because the owner was not the singleton class of the receiver, the rule counted it as an ancestor; so two different authors work in the same declaration line. The method of the `field` call was written by an ancestor, and the `price` method that call gave birth to was written by code. **MP31.** This distinction rests on the singleton class rules of the `author` helper; another helper could classify differently.

This must not be confused with the question in M08/K06. There the question was the cause of a behavior in the source: a call, a comment or a default. Here the cause is not asked; the author of the definition is. `field :price, 0` is a call, but the author of the method it gave birth to is code.

## Reading Leaves No State

Whether the generated method has a side effect can be measured too. The default is computed inside the method; it is not written to the object.

```ruby
# read.rb — the trace the reader leaves on the object
require_relative "model"

pr = Product.new
p pr.price
p pr.instance_variables
p pr.method(:price).owner
p pr.method(:price).arity
```
```
0
[]
Product
0
```

`price` was read and gave `0`, but the list of the object's instance variables stayed empty: the default came back as a value and was not stored as a field. The owner of the method is `Product`, so the declaration put the definition in the subclass, not in `Model`. The arity is zero: the block given to `define_method` takes no parameter, so the generated reader takes none either. These three observations complete what the declarative interface wrote into the class: a new method, the subclass as its owner, and no state. **MP32.** This measurement is only in the reading direction; there is no method in this `field` definition that writes the value, and the value was given only with `instance_variable_set`.

## Where a Class Method Is Looked Up

Why the `Product.field` call reaches `Model` can be read from the singleton class chain.

```ruby
# chain.rb — the chain of class methods
require_relative "model"

p Product.singleton_class.ancestors.take(3)
p Model.singleton_class.instance_methods(false)
p Product.respond_to?(:field)
p Product.new.respond_to?(:field)
```
```
[#<Class:Product>, #<Class:Model>, #<Class:Object>]
[:field]
true
false
```

The first link of the chain is the own singleton class of `Product`, and the second is that of `Model`. Class methods come by inheritance because the singleton class of a subclass descends from the singleton class of the superclass. `Product.field` was found in the second link of this chain. Writing the declaration method in the base is therefore enough: every subclass can call it in its own body. In the singleton class of `Model` there is a single method, `field`. The class `Product` answers it (`true`), but an object of `Product` does not (`false`): the declaration belongs to the interface of the class, not to the interface of the object. The object sees only the `name` and `price` methods that the declaration gave birth to. This distinction also says why a domain-specific language lives in the class body: while the declaration is being written, what is spoken to is the class, not the object. **MP33.** The output shows only the first three links; the rest of the chain was not measured in this lesson.

## The Block Form

For taking declarations out of the class body there is another setup: a block is given, and the block is run in the context of an object.

```ruby
# form.rb — the block form with instance_eval
class Form
  attr_reader :fields

  def self.build(&block)
    f = new
    f.instance_eval(&block)
    f
  end

  def initialize
    @fields = []
  end

  def field(name)
    @fields << name
  end
end

f = Form.build do
  field :name
  field :email
end
p f.fields
```
```
[:name, :email]
```

The receiver of the `field` calls inside the block was not written; `instance_eval` ran the block in the context of the `Form` object, which means the `self` of the block became that object. The result is `[:name, :email]`. This form is readable: the user writes only the words, without writing an object name. Readability has a price, and it is measured below. **MP34.** There was a single `field` method and two calls here; no other method calls inside the block are in this measurement.

## The Bounding Measurement

`instance_eval` changes the `self` of the block. What happens to the variables outside the block? Three are looked at: an instance variable defined outside, a local variable and `self` itself. Then the `yield` form of the same setup is tried.

```ruby
# limit.rb — instance_eval and outer variables
@title = :record
local_name = :local

class Form
  attr_reader :fields

  def self.build(&block)
    f = new
    f.instance_eval(&block)
    f
  end

  def initialize
    @fields = []
  end

  def field(name)
    @fields << name
  end
end

class Form2
  attr_reader :fields

  def self.build
    f = new
    yield f
    f
  end

  def initialize
    @fields = []
  end

  def field(name)
    @fields << name
  end
end

p Form.build { field @title }.fields
p Form.build { field local_name }.fields
p Form.build { field self.class }.fields
p Form2.build { |f| f.field @title }.fields
```
```
[nil]
[:local]
[Form]
[:record]
```

There are three results. The outer `@title` gave `nil` inside the block: instance variables are tied to `self`, and `self` changed. The local variable `local_name` gave `[:local]`: local variables are caught by the closure of the block and are not tied to `self`. `self.class` came out `Form`. The `yield` form on the last line kept `self`, and the value of `@title` was read as `[:record]`. So `instance_eval` gains vocabulary but makes the outer instance variables invisible. **MP35.** This difference was measured only for instance variables; the behavior of constants and class variables was not tested in this lesson.

The declaration also has a silent price: declaring the same field twice raises no error.

```ruby
# repeat.rb — declaring the same field twice
require_relative "model"

class Label < Model
  field :name, "unnamed"
  field :name, "new"
end

p Label.new.name
p Label.instance_methods(false)
```
```
"new"
[:name]
```

The second declaration wrote over the first: `name` now gives `"new"`, and there is still a single `name` method in the class. There is no warning and no exception. The declarative interface is therefore easy to write and easy to write wrongly; `define_method` silently redefines the same name. **MP36.** This measurement is the case where both declarations are in the same class; had they been at different inheritance levels, the subclass would have shadowed the method of the superclass.

## Summary

- The line `field :price, 0` is a call; the author of the `price` method it gave birth to is **code**, because `define_method` produces a block body.
- The declaration method `field` is written in `Model` and, being a class method, is looked up in the singleton class chain of `Product`; the author of that method is an **ancestor**.
- `instance_eval` changes the `self` of the block; outer instance variables become `nil`, and local variables stay visible.
- An object passed with `yield` keeps `self`; for the sake of vocabulary, outer instance variables are given up.
- Declaring the same field twice raises no error; the later declaration silently replaces the earlier method.
- The author of this lesson is code: the lines of a declarative interface belong to the programmer, and the methods they give birth to belong to code.

## Course Wrap-Up

Every lesson of the course asked the same question: who wrote this method? Nine observations answered this question nine times with a measurement. The table below collects them all by author category.

| Lesson | Measured method | Author | Bounding measurement |
| --- | --- | --- | --- |
| Class Definition and Instance Variables | `account.balance_text` | programmer | `attr_reader` is a second author in the same class |
| Inheritance and `super` | `TermDeposit.new("tom", 50).balance_text` | ancestor | a redefinition that calls `super` is programmer |
| Modules and Mixins | `account.report` | ancestor | the class's `def` shadows the mixin |
| Visibility | `fee` (from inside `account.net`) | programmer | `public :fee` moves the owner of the definition to the subclass |
| The Method Lookup Chain | `Safe.new.unlock` | ancestor | a singleton method on one object goes first |
| Reflection | `Point.new(3, 4).x` | code | core methods also look like code |
| Dynamic Method Definition | `Settings.new.color` | code | a `class_eval` string comes out programmer |
| Catching Missing Methods | `Proxy.new([3, 1, 2]).sort` | nobody | nobody turns into code on the first call |
| Domain-Specific Languages | `Product.new.price` | code | `instance_eval` changes `self` |

The distribution of the authors: **3 ancestor · 3 code · 2 programmer · 1 nobody**. The table carries one finding of the course: the author was measured per method, not per class, and the author of a method was decided not by how it looks in the source text but by how it is found in the chain. Two authors appeared in the same class, and two authors appeared in the same line.

Two inferences can be drawn. The first is that knowing the author does not require knowing the way of writing: `attr_reader` and `define_method` are different words, but both make code the author; `def` and a `class_eval` string are different words, but both make the programmer the author. The second is that reflection is not enough to tell the author: `source_location`, `owner` and `respond_to?` can each be misleading on their own; the rule read them together. The category nobody fell on a single example, and that one was not permanent. A third inference comes from the distribution itself: in only two of nine observations is the author the programmer. In the measured classes, most of the interface came from ancestors, mixins and definitions produced by code; looking at the class body is therefore not enough by itself to find the author, and the chain and the form of the definition must be measured.

**MP37.** The table is only the classification of nine measurements; the count of the categories reflects the choice of measured methods, not the general distribution of methods in Ruby. The author cells were taken from the statement in the Summary section of each lesson.

The course ends here. The next course takes up the four layers of a web application written in Ruby, in order: routing, controller, model and view. There the same rule stays in force: nothing is guessed, everything is measured.
