‘Iacta alea est’
In a previous post, I introduced Hypertext’s DSL which is a thin, experimental layer around the core Hypertext API designed to reduce the boilerplate for defining a hypertext document (or fragment) in Ruby.
It dynamically defines a method for each HTML5 tag which acts as a wrapper for the corresponding Hypertext#tag
method and arguments. For example, for the head
tag it replaces this:
# Hypertext Core
h.tag :html, lang: "en-GB" do
…
end
with this:
# Hypertext DSL
html lang: "en-GB" do
…
end
There are two key things going on under the hood. The first is the method definition mentioned above, which looks like this:
TAGS.each do |tag_name|
define_method(tag_name) do |attributes = {}, &block|
@ht.tag(tag_name, attributes, &block)
end
end
The TAGS
constant referenced above is simply an array of symbols representing the set of HTML5 tags included in the DSL:
TAGS = [:a, :abbr, :address, :area, :article, … ]
You can see that the resulting methods are simply delegating to the Hypertext#tag
method, passing in the tag name (which is then the method name in the DSL) as the first argument, and passing in the attributes and block as the second and third arguments. If you squint it looks an awful lot like method currying, but then with a method name later becoming an argument.
The second important aspect has to do with the @ht
instance variable referenced in these method definitions. This is an instance of the Hypertext
class, exposing the core API to the DSL. This object is instantiated on DSL initialization and assigned to @ht
and then the block passed to the DSL is executed in the context of the receiver via instance_eval
, thus allowing all methods to access the instance variable @ht
and exposing the HTML tag methods without the need for passing self
and calling the methods on that argument to the block.
Hypertext::DSL.new do
html lang: "en-GB" do
…
end
end.to_s
is calling this code:
def initialize(&block)
@ht = Hypertext.new
instance_eval(&block)
end
The only other two methods in the DSL class are text
and to_s
which are both also transparently delegating to the corresponding core API methods:
def text(content)
@ht.text(content)
end
def to_s
@ht.to_s
end
Believe it or not, aside from the long list of HTML tags which I truncated for readability, we’ve now seen all of the code from the DSL in this blog post. You can checkout the full 55 SLOC on Hypertext’s GitHub repository.
For me, this is the benefit of the trade-off I mentioned in the previous post. By choosing for a pure Ruby DSL, the implementation is radically simplified.
What is also a triumph of good design, in my eyes at least, is that the core can be used completely independently of the DSL. It stands on its own. The DSL is a layer on top of the core.
I don’t know if this was intentional from the start, but it proves to me that having solid primitives allows for judicious extension. That building the foundation thoughtfully and carefully enables higher-level design that doesn’t have to resort to building anew.
That’s something worth striving for.
—Sunday 21st March 2021.