uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Groovyで日本語DSL再び(+GEP3の解説)

注意:以下の内容はGroovy 1.8のβ(9/19時点のtrunk)で試した結果を元にしています。最終版では異なってくる可能性があります。

前に「Groovyの日本語DSL」という記事を書きましたが、前に紹介したGEP3の機能(Groovy 1.8系でのみ利用可能)を使って、括弧やピリオドを省けるようにしたバージョンを作ってみました。
山本さんが似たような事をやってたのに触発されました。)

Object.metaClass.を =
  Object.metaClass.の =
   { clos -> clos.call(delegate) }

最初に=まず=そして=次に=続いて=
 さて=ところで=最後に=それはそれとして=ついでに=とりあえず={ it }

表示する={ println it }
平方根={ Math.sqrt(it) }

Integer.metaClass.と = { [delegate,it] }
Object.metaClass.と = { delegate+it }
Object.metaClass.から = { delegate .. it }

Object.metaClass.のうち、 = { clos->delegate.grep(clos)}
Object.metaClass.それぞれに = { clos-> delegate.each(clos) }
Closure.metaClass.かつ = { clos ->
   thisClosure = delegate
   return { n -> thisClosure.call(n) && clos.call(n) }
}

対して = { it }
奇数 = { it % 2 == 1 }
二倍にした = { it*2 }
倍数判定 = { it,n -> it % n == 0 }
三の倍数 = 倍数判定.rcurry(3)
二の倍数 = 倍数判定.rcurry(2)

// ---ここから日本語DSL(GEP3 Version)---

まず 100 の 平方根 を 表示する
// ==> 10.0

そして 1 と 2 と 3 のうち、 奇数 それぞれに 対して { それ ->
  二倍にした それ を 表示する
}
// ==> 2,6

それはそれとして "abc" を 表示する
// ==> "abc"

ついでに 1 から 100 のうち、 二の倍数.かつ(三の倍数) を 表示する
// ==> [6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, 90, 96]

最後に 1 から 100 のうち、 二の倍数.かつ(三の倍数) それぞれに 対して { それ ->
  とりあえず それ + "は6の倍数です" を 表示する
}
// ==>
// 6は6の倍数です
// 12は6の倍数です
// 18は6の倍数です
// :
//84は6の倍数です
//90は6の倍数です
//96は6の倍数です

こちらで実行を試せます

ピリオドや括弧を省かないと以下のようになります。以下であれば、Groovy 1.7系でも実行可能なはずです(試してませんが)。

まず(100).の(平方根).を(表示する)
そして(1).と(2).と(3).のうち、(奇数).それぞれに(対して({ それ ->
  二倍にした(それ).を(表示する)
}))
それはそれとして("abc").を(表示する)
ついでに(1).から(100).のうち、((二の倍数.かつ(三の倍数))).を(表示する)
最後に(1).から(100).のうち、(二の倍数.かつ(三の倍数)).それぞれに(対して({ それ ->
  とりあえず(それ + "は6の倍数です").を(表示する)
}))

GEP3の省略ルールは、理解するまではちょっとだけ分かりにくいのですが、まず基本はCommand expressionつまり式文のメソッド呼び出しである必要があって*1、さらに

  • 初項はメソッド呼び出し:
    • メソッド(引数)
    • xx.yy.メソッド(引数)
  • 次項以降は、「 .メソッド(引数) 」の繰り返し

のときに初項の括弧及び次項以降の部分でのピリオドと括弧が省略できます。例えば

m1(arg).m2(arg).m3(arg)

m1 arg m2 arg m3 arg

というようになります。上で引数は複数指定する事もでき、例えば

m1 a1,a2,a3 m2 a4,a5,a6 m3 a7,a8,a9

などは問題ありません。引数は任意の式が指定可能です。結果的に、 ホワイトスペースで区切られていくものは、「メソッド + 引数」のペアなので必ず偶数個になります。

クロージャの即値を含められるのは、

m1(arg).m2(arg).m3(m4( { closure} ))

のパターンなどで、上は

m1 arg m2 arg m3 m4 { closure }

と書けます。つまり、argの1つにするのではなく、いったんm4というメソッド呼び出しを介在させる必要があります。
この場合、クロージャが呼び出しチェインの中には組み込まれないので使い勝手が良くないのですが、m4を引数のクロージャをそのまま返すメソッドにして、m3で受け取るという技を使うことで組み込む事ができます(上の例では「対して」でこの技を使っています)。

もしくは、引数が単一でなければ、

m1(a,{closure}))

などを

m1 a,{ closure }

のように引数の一つとしてクロージャを置くこともできます。

なお、全体としてGEP3の記法は、「(動詞/前置詞) + 名詞 」の繰り返しであるような英語表現をうまく書く事に特化してるので、日本語に適用するのにはちょっとだけ頭をひねる必要があります。今回の日本語DSLで接続語やクロージャを多用してるのはそういう事情によります。

と書いてきて、これがDSLなのかと言われると違う気もしてきた。単なる日本語プログラミングか。

*1:もともとcommand expressionでは単一のメソッド呼び出しの括弧は省略できていたのですが、GEP3では複数の項からなる呼び出しのチェインにおいての括弧も省略できるように拡張されたということです。