みなさんこんにちは!
この記事では、Kotlinツールを使用して正規表現用に特定のDSL( ドメイン固有言語 、ドメイン固有言語)を実装する方法について説明しますが、KotlinでDSLを記述する方法と、通常 「内部で」行うことの概要を説明します「同じ言語機能を使用する他のDSL。
多くの人がすでにKotlinを使用しているか、少なくとも試してみましたが、他の人は、KotlinにはエレガントなDSLを作成する力があることを聞いたことがあるかもしれません-Ankoとkotlinx.html 。
もちろん、これは既に正規表現で行われています (また、Java 、 Scala 、 C#で -多くの実装があり、これは一般的な娯楽のようです)。 しかし、Kotlin DSL指向の言語機能を練習または試してみたい場合は、catにようこそ。
Kotlinで書かれたDSLは通常どのようなものですか?
最悪の場合、おそらくそうです。
ほとんどのJava DSLは、次のJava Regex DSLの例のように、構造に呼び出しチェーンを使用することを提案しています。
Regex regex = RegexBuilder.create() .group("#timestamp") .number("#hour").constant(":") .number("#min").constant(":") .number("#secs").constant(":") .number("#ms") .end() .build();
このアプローチを使用することもできますが、いくつかの不都合があり、そのうち2つはすぐに注意できます。
ネストされた構造(上記の
group
とend
)の実装が不便であるため、フォーマッターと戦わなければならず、開始要素と終了要素のコンプライアンスの平凡な検証がないため、追加の.end()
書くことができます。
- 式を動的に生成する機会が乏しい:条件を確認するなど、クエリに次の部分を追加する前に任意のコードを実行する場合、呼び出しチェーンを中断し、部分的に作成された式を変数に保存する必要があります。
Type-Safe Groovy-Style BuilderのスタイルでDSLを実装する場合、Kotlinはこれらの欠点に対処できます(技術的な詳細を説明するとき、この記事では参照によりドキュメントページを繰り返します)。 次に、その上のコードはこのAnkoの例のようになります。
verticalLayout { val name = editText() button("Say Hello") { onClick { toast("Hello, ${name.text}!") } } }
または、このkotlinx.htmlの例:
html { body { div { a("http://kotlinlang.org") { target = ATarget.blank +"Main site" } } } }
今後は、結果の言語は次のようになります。
val r = regex { group("prefix") { literally("+") oneOrMore { digit(); letter() } } 3 times { digit() } literally(";") matchGroup("prefix") }
さあ始めましょう
class RegexContext { } fun regex(block: RegexContext.() -> Unit): Regex { throw NotImplementedError() }
ここに何が書かれていますか? regex
関数は、構築された Regex
を返し、1つの引数RegexContext.() -> Unit
タイプの別の関数)を受け入れRegexContext.() -> Unit
。 すでにKotlinに精通している場合は、それが何であるかを説明するいくつかのパラグラフをスキップしてください。
Kotlin の関数のタイプは次のように記述されます: (Int, String) -> Boolean
は2つの引数の述語です-またはこのように: SomeType.(Int) -> Unit
はUnit (void関数の類似物) を返す関数です Int
引数は、 SomeType
型のレシーバーも受け入れます。
このタイプのラムダ式を引数として渡すことができ、レシーバーと同じタイプの暗黙のthis
を持つため、レシーバーを受け取る関数はDSLの構築に非常に役立ちます。 簡単な例は、次のライブラリ関数です。
fun <T, R> with(t: T, block: T.() -> R): R = t.block() // block receiver // , with(ArrayList<Int>()) { for(i in 1..10) { add(i) } // add receiver println(this) // this -- ArrayList<Int> }
これで、まるでthisであるかのように、 regex { ... }
および中括弧内でRegexContext
インスタンスをRegexContext
this
。 残っている唯一の小さなRegexContext
は、 RegexContext
メンバーを実装することRegexContext
。 :)
なぜRegexContextが必要なのですか?
部分で正規表現を作成しましょう-DSLの各ステートメントは、未完成の表現に別の部分を追加するだけです。 これらの部分もRegexContext
によって保存されRegexContext
。
class RegexContext { internal val regexParts = mutableListOf<String>() // private fun addPart(part: String) { // regexParts.append(part) } }
したがって、 regex {...}
関数は次のようになります。
fun regex(block: RegexContext.() -> Unit): Regex { val context = RegexContext() context.block() // block, - context val pattern = context.regexParts.toString() return Regex(pattern) // - Regex }
次に、正規表現にさまざまな部分を追加するRegexContext
関数を実装します。
以下の関数は、特に明記されていない限り、クラスの本体にもあります。
すべてが非常に簡単です
そう?
fun anyChar(s: String) = addPart(".")
この呼び出しは、式にドットを追加するだけです。ドットは、任意の1文字に対応する部分式を示します。
同様に、関数digit()
、 letter()
、 alphaNumeric()
、 whitespace()
、 wordBoundary()
、 wordCharacter()
、さらにstartOfString()
およびendOfString()
をendOfString()
します。これらはすべてほぼ同じに見えます。
fun digit() = addPart("\\d") fun letter() = addPart("[[:alpha:]]") fun alphaNumeric() = addPart("[A-Za-z0-9]") fun whitespace() = addPart("\\s") fun wordBoundary() = addPart("\\b") fun wordCharacter() = addPart("\\w") fun startOfString() = addPart("^") fun endOfString() = addPart("$")
しかし、正規表現に任意の文字列を追加するには、最初に文字列に含まれる文字が正式な文字として解釈されないように変換する必要があります。 これを行う最も簡単な方法は、 Regex.escape(...)関数を使用することです。
fun literally(s: String) = addPart(Regex.escape(s))
たとえば、 literally(".:[test]:.")
\Q.:[test]:.\E
literally(".:[test]:.")
は、式に\Q.:[test]:.\E
部分を追加します。
もっと深く
量指定子はどうですか? 明らかな観察:量指定子は部分式に掛けられており、それ自体も有効な正規表現です。 少しネストを追加しましょう!
次のような入れ子になったコードブロックで中括弧で量指定子の副次式を設定します。
val r = regex { oneOrMore { optional { anyChar() } literally("-") } literally(";") }
これは、 regex {...}
とほぼ同じ動作をするRegexContext
関数の助けを借りてRegexContext
ますが、それ自体は構築された部分式を使用します。 最初にヘルパー関数を追加します。
private fun addWithModifier(s: String, modifier: String) { addPart("(?:$s)$modifier") // non-capturing group } private fun pattern(block: RegexContext.() -> Unit): String { // -- val innerContext = RegexContext() innerContext.block() // block RegexContext return innerContext.regexParts.toString() // }
そして、それらを使用して「数量詞」を実装します。
fun optional(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "?") fun oneOrMore(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "+")
fun oneOrMore(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "+") fun oneOrMore(s: String) = oneOrMore { literally(s) } fun optional(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "?") fun optional(s: String) = optional { literally(s) } fun zeroOrMore(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "*") fun zeroOrMore(s: String) = zeroOrMore { literally(s) }
正規表現には、予想される出現回数を正確に設定する機能や、範囲を使用する機能があります。 これも自分でやりたいですよね? これは、 挿入関数を使用する正当な理由でもあります-2つの引数の関数で、そのうちの1つはレシーバです。 このような関数の呼び出しは次のようになります。
val r = regex { 3 times { anyChar() } 2 timesOrMore { whitespace() } 3..5 times { literally("x") } // 3..5 -- IntRange }
そして、関数自体は次のように宣言されます:
infix fun Int.times(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "{$this}") infix fun IntRange.times(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "{${first},${last}}")
infix fun Int.times(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "{$this}") infix fun Int.times(s: String) = this times { literally(s) } infix fun IntRange.times(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "{${first},${last}}") infix fun IntRange.times(s: String) = this times { literally(s) } infix fun Int.timesOrMore(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "{$this,}") infix fun Int.timesOrMore(s: String) = this timesOrMore { literally(s) } infix fun Int.timesOrLess(block: RegexContext.() -> Unit) = addWithModifier(pattern(block), "{0,$this}") infix fun Int.timesOrLess(s: String) = this timesOrLess { literally(s) }
グループアップ!
正規表現を操作するためのツールは、グループをサポートしていない場合は呼び出すことができないため、たとえば次の形式でそれらをサポートしましょう。
val r = regex { anyChar() val separator = group { literally("+"); digit() } // anyChar() matchGroup(separator) // anyChar() }
ただし、グループは正規表現構造に新しい複雑さを追加します。それらは左から右に「右から」番号が付けられ、部分式のネストを無視します。 これは、 group {...}
呼び出しを互いに独立して考えることは不可能であることを意味しgroup {...}
さらに、ネストされたすべての部分式も相互に接続されます。
グループの番号付けをサポートするには、 RegexContext
わずかに変更しRegexContext
。これにより、既にグループがいくつあるかが記憶されます。
class RegexContext(var lastGroup: Int = 0) { ... }
そして、ネストされたコンテキストがそれらの前にいくつのグループがあるかを知り、それらの中にいくつのグループが追加されたかを知るために、 pattern(...)
関数を変更します:
private fun pattern(block: RegexContext.() -> Unit): String { val innerContext = RegexContext(lastGroup) // innerContext.block() lastGroup = innerContext.lastGroup // return innerContext.regexParts.toString() }
現在、 group
を正しく実装することを妨げるものはありません:
fun group(block: RegexContext.() -> Unit): Int { val result = ++lastGroup addPart("(${pattern(block)})") return result }
名前付きグループの場合:
fun group(name: String, block: RegexContext.() -> Unit): Int { val result = ++lastGroup addPart("(?<$name>${pattern(block)})") return result }
そして、インデックス付きと名前付きの両方のグループ一致:
fun matchGroup(index: Int) = addPart("\\$index") fun matchGroup(name: String) = addPart("\\k<$name>")
他に何か?
はい! 正規表現の重要な構成-代替案-を忘れていました。 リテラルの場合、代替手段は簡単に実装されます。
fun anyOf(vararg terms: String) = addPart(terms.joinToString("|", "(?:", ")") { Regex.escape(it) }) // terms , , // Regex.escape(...)
ネストされた式の複雑な実装はありません:
fun anyOf(vararg blocks: RegexContext.() -> Unit) = addPart(blocks.joinToString("|", "(?:", ")") { pattern(it) })
fun anyOf(vararg characters: Char) = addPart(characters.joinToString("", "[", "]").replace("\\", "\\\\").replace("^", "\\^")) fun anyOf(vararg ranges: CharRange) = addPart(ranges.joinToString("", "[", "]") { "${it.first}-${it.last}" })
しかし、同じanyOf(...)
代替として異なるものanyOf(...)
たとえば、ネストされた部分式のコードを含む文字列とブロックanyOf(...)
を使用する場合はどうでしょうか。 ここで少し失望するのを待っています。Kotlinにはユニオン型(ユニオン型)がなく、引数型String | RegexContext.() -> Unit | Char
String | RegexContext.() -> Unit | Char
String | RegexContext.() -> Unit | Char
できません。 松葉杖でまだDSLを良くしない恐ろしい外観でこれを回避することができたので、上記のようにすべてを残すことにしました-最終的に、 String
とChar
両方は、対応するオーバーロードanyOf {...}
anyOf(vararg parts: Any)
使用しますanyOf(vararg parts: Any)
は、オブジェクトが属する型です。 どの型がそれぞれ内部にあるかを確認し、IllegalArgumentException
を、不当な引数を渡した不注意なユーザーにスローします。
ハードコア Kotlinでは、クラスはinvoke()演算子をオーバーライドでき、このクラスのオブジェクトは関数として使用できます:
myObject(arg)
、演算子にいくつかのオーバーロードがある場合、オブジェクトは関数のいくつかのオーバーロードのように動作します。 その後、anyOf(...)
関数をカリー化できますが、引数の数は任意であるため、いつ終了するかわかりません。したがって、各部分アプリケーションは前の結果をキャンセルし、引数が最後であるかのようにそれ自体を適用する必要があります。
これをきちんと行えばうまくいきますが、Kotlin文法で不愉快な瞬間にぶつかります。中括弧での
invoke
は、invoke
演算子チェーンの行で使用できません。
object anyOf { operator fun invoke(s: String) = anyOf // operator fun invoke(r: RegexContext.() -> Unit) = anyOf } anyOf("a")("b")("c") // anyOf("123") { anyChar() } { digit() } // ! anyOf("123")({ anyChar() })({ digit() }) // ((anyOf("123")) { anyChar() }) { digit() } //
まあ、私たちはそれが必要ですか?
さらに、DSLで作成された正規表現と、どこかから来た正規表現の両方を再利用するのもいいでしょう。 これは難しくありません。主なことは、グループの番号付けを忘れないことです。 そのグループの数はPattern.compile(pattern).matcher("").groupCount()
から取得できますPattern.compile(pattern).matcher("").groupCount()
、残りは対応するRegexContext
関数を実装することRegexContext
です:
fun include(regex: Regex) { val pattern = regex.pattern addPart(pattern) lastGroup += Pattern.compile(pattern).matcher("").groupCount() }
そして、これで、おそらく、必須の機能は終了します。
おわりに
最後まで読んでくれてありがとう! 私たちは何をしましたか? 使用できる正規表現用の完全に実行可能なDSL:
fun RegexContext.byte() = anyOf({ literally("25"); anyOf('0'..'5') }, { literally("2"); anyOf('0'..'4'); digit() }, { anyOf("0", "1", ""); digit(); optional { digit() } }) val r = regex { 3 times { byte(); literally(".") } byte() }
(質問:この正規表現は何のためですか?本当に簡単ですか?)
その他の利点:
- 正規表現を破ることは困難です。コードをコンパイルし、グループが正しい場合、正規表現も有効であれば、手で角括弧を書く必要さえありません。
- 視覚的に正規表現を動的に形成することが判明しました。条件、ループ、サードパーティ関数の呼び出しなどの有効な構成を使用して、生きたコードを作成します。
- インデックス付きグループを使用する場合、インデックスはグループに動的に割り当てられ、DSLで記述された大きな正規表現を変更しても、グループインデックスは破損しません。
- 拡張性と再利用性:上記の
byte()
と同様に、コード内に任意の拡張関数を記述し、正規表現で不可欠な部分として使用できますrussianLetter()
、russianLetter()
ipAddress()
、time()
...
うまくいかなかったもの:
- AnyOf
anyOf(...)
はanyOf(...)
に見えますが、最高を達成することはできませんでした。 - 記録密度は、従来の形式よりもはるかに劣っています。半分の画面の長さの正規表現は、半分の画面の高さのブロックに変わります。 しかし、おそらく読みやすい。
プロジェクトに追加する準備ができたソース、テスト、依存関係-Githubのリポジトリー 。
正規表現のサブジェクト指向言語についてどう思いますか? 使用したことがありますか? 他のDSLはどうですか?
聞こえたすべてのものについて議論したいです。
頑張って