背景
JenkinsというCIツールではGroovyのコードでCI挙動を記述できるJenkins pipelineがあります。
ここではGroovyのDSL(Domain-Specific Language:ドメイン固有言語)が使われています。
DSLにはクロージャが効果的に使用されています。
今回は独自のDSLの作成です。
前回の記事
前回の記事で、
クロージャの名前解決の方法の各挙動を確認しました。
yucatio.hatenablog.com
今回は、クロージャと名前解決方法を利用して、独自のDSLを定義します。
DSLとは特定のタスク向けに設計されたコンピュータ言語である。
ドメイン固有言語 - Wikipedia
例えば、文章の構造を表すDSL(XMLなど)やページの装飾に特化したDSL(CSSなど)、データの取得に特化したDSL(SQLなど)が挙げられます。
特定のタスクに向けて設計されるDSLは、プログラミング経験者以外が使用することを前提としたものもあります。また、人が自然に読めるように配慮されているものも多くあります。
記述には、宣言型のスタイルがよく用いられます。
宣言型のスタイル(宣言型プログラミング)とは、
対象の性質を宣言してプログラムを構成するプログラミングパラダイム、あるいはそのような性質をもったプログラミングパラダイムの総称である。
宣言型プログラミング - Wikipedia
宣言型プログラミングでは、どのように処理をするかは書かれておらず、何を処理するか(対象)を記述(宣言)します。
例: SQLの場合
SELECT id, name, price FROM item WHERE price >= 100
GroovyでのDSLの実現方法
Groovyでは以下のようなDSLを作成できます。何を表すか(するか)という短い説明の後に、内容を記載します。
子要素がある場合は{
と}
で囲います。
report {
title "DSLの説明"
date "2020-02-03"
body {
section("クロージャとは") {
paragraph "クロージャとは生まれ故郷(クロージャ自身が定義された場所)を忘れない無名関数のことです。"
}
section("thisとownerとdelegate") {
paragraph "thisはクロージャを囲んでいるクラスです。"
paragraph "ownerはクロージャを囲んでいるオブジェクトです。クラスか、クロージャを囲んでいるクロージャです。"
paragraph "delegateは外部のオブジェクトです。メソッドやプロパティ名が元のオブジェクトで解決できないときに呼ばれます。"
}
}
}
この記法はプログラミング言語っぽくないですが、groovyの言語仕様に従って記載されています。{
と}
で囲まれた部分はクロージャです。report
やtitle
、section
などはメソッドです。
今回は以下のような簡単なDSLを作成します。
- reportディレクティブはレポートを作成するためのトップ階層です
- reportの下にはtitle、date、bodyのみ記載することができます
- title、dateには文字列を続けて書きます
- bodyの子要素はsectionのみです
- sectionの子要素はparagraphのみです
- paragraphには文字列を続けて書きます
reportメソッドの作成
report
メソッドは以下のようになります。クロージャのdelegate
にReportSpec
クラスのインスタンスを指定し、resolveStrategy
をClosure.DELEGATE_ONLY
に設定します。
def report(Closure cl) {
cl.delegate = new ReportSpec()
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl()
}
移譲先のクラスの作成
ReportSpec
クラスです。title
メソッドとdate
メソッドは対応する文字列を表示します。body
メソッドは、クロージャを引数にとり、BodySpec
クラスのインスタンスをdelegate
に設定しています。
class ReportSpec {
void title(String title) { println "<h1>${title}</h1>"}
void date(String date) { println "<div>作成日 : ${date}</div>"}
void body(Closure body) {
body.delegate = new BodySpec()
body.resolveStrategy = Closure.DELEGATE_ONLY
body()
}
}
同様に、BodySpec
は、以下です。
class BodySpec {
void section(String title, Closure section) {
println "<h2>${title}</h2>"
section.delegate = new SectionSpec()
section.resolveStrategy = Closure.DELEGATE_ONLY
section()
}
}
同様に、SectionSpec
は、以下です。
class SectionSpec {
void paragraph(String paragraph) {println "<p>${paragraph}</p>"}
}
実行結果
以下のDSLを実行します。
report {
title "DSLの説明"
date "2020-02-03"
body {
section("クロージャとは") {
paragraph "クロージャとは生まれ故郷(クロージャ自身が定義された場所)を忘れない無名関数のことです。"
}
section("thisとownerとdelegate") {
paragraph "thisはクロージャを囲んでいるクラスです。"
paragraph "ownerはクロージャを囲んでいるオブジェクトです。クラスか、クロージャを囲んでいるクロージャです。"
paragraph "delegateは外部のオブジェクトです。メソッドやプロパティ名が元のオブジェクトで解決できないときに呼ばれます。"
}
}
}
実行結果です。
<h1>DSLの説明</h1>
<div>作成日 : 2020-02-03</div>
<h2>クロージャとは</h2>
<p>クロージャとは生まれ故郷(クロージャ自身が定義された場所)を忘れない無名関数のことです。</p>
<h2>thisとownerとdelegate</h2>
<p>thisはクロージャを囲んでいるクラスです。</p>
<p>ownerはクロージャを囲んでいるオブジェクトです。クラスか、クロージャを囲んでいるクロージャです。</p>
<p>delegateは外部のオブジェクトです。メソッドやプロパティ名が元のオブジェクトで解決できないときに呼ばれます。</p>
レポートが作成されました。
rehydrateメソッドの使用
今回、report
, reportSpec
, BodySpec
内で、クロージャのdelegate
を以下のように指定しました。
def report(Closure cl) {
cl.delegate = new ReportSpec()
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl()
}
一般的に、引数として受け取った変数の内容は変更すべきではありません。
今回は引数として受け取ったクロージャのdelegate
フィールドを書き換えていたので、この制約を破っています。
この状態を避けるために、クロージャをコピーし、コピーしたものに対してdelegate
を変更します。
幸いなことに、コピーとdelegate
の変更を一度に行うrehydrate
というメソッドが用意されています。
(hydrateは水分を補給するという意味)
rehydrate
ではクロージャをコピーして、owner
とthis
とdelegate
を引数で与えられたオブジェクトに変更したものを返します。
report
メソッドをrehydrate
で書き換えます。owner
とthisObj
の部分にthis
を指定していますが、下で名前解決方法をClosure.DELEGATE_ONLY
に指定しているので、owner
とthis
の部分は何でも構いません。
def report(Closure cl) {
def reportSpec = new EmailSpec()
def clCopy = cl.rehydrate(reportSpec, this, this)
clCopy.resolveStrategy = Closure.DELEGATE_ONLY
clCopy()
}
以上で独自のDSLの作成できました。
参考リンク
The Apache Groovy programming language - Domain-Specific Languages