Lesson 09 / 09
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.
Published Updated
In this lesson
- 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
Contents
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.
# 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.
# 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.
# 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.
# 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.
# 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.
# 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.
# 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.
# 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.
# 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, 0is a call; the author of thepricemethod it gave birth to is code, becausedefine_methodproduces a block body. - The declaration method
fieldis written inModeland, being a class method, is looked up in the singleton class chain ofProduct; the author of that method is an ancestor. instance_evalchanges theselfof the block; outer instance variables becomenil, and local variables stay visible.- An object passed with
yieldkeepsself; 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.