Kotlinの正規表現用のDSL





みなさんこんにちは!







この記事では、Kotlinツールを使用して正規表現用に特定のDSL( ドメイン固有言語 、ドメイン固有言語)を実装する方法について説明しますが、KotlinでDSLを記述する方法と、通常 「内部で」行うことの概要を説明します「同じ言語機能を使用する他のDSL。







多くの人がすでにKotlinを使用しているか、少なくとも試してみましたが、他の人は、KotlinにはエレガントなDSLを作成するがあることを聞いたことがあるかもしれません-Ankokotlinx.html







もちろん、これは既に正規表現で行われています (また、JavaScalaC#で -多くの実装があり、これは一般的な娯楽のようです)。 しかし、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つはすぐに注意できます。









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() }
      
      





(質問:この正規表現は何のためですか?本当に簡単ですか?)







その他の利点:









うまくいかなかったもの:









プロジェクトに追加する準備ができたソース、テスト、依存関係-Githubリポジトリー







正規表現のサブジェクト指向言語についてどう思いますか? 使用したことがありますか? 他のDSLはどうですか?







聞こえたすべてのものについて議論したいです。







頑張って








All Articles