2ステップでKotlinに簡単なDSLを書く

画像


DSL(ドメイン固有言語)-特定のアプリケーション分野に特化した言語Wikipedia


なぜKotlinが嫌いなのかという記事は、この投稿を書くきっかけになりました。 しばらくの間、Kotlinでプログラミングをしていましたが、できないが、本当にしたいのなら、できるという印象を受けました。 そして、データ構造を記述するためにDSLを書くことにしました。 これがその結果です。


免責事項


構造を記述するためにDSLを取得したいという事実にもかかわらず、この記事の目的は、まず、この非常にDSLを記述できるようになるKotlin言語の機能を(例とともに)正確に説明することです。 もちろん、単純なDSLを作成します:)


構文


簡単にするため、または個人的な好みのために、将来のDSLの構文でJSONに似たデータ構造を記述したいと考えています。 つまり、構文は次のことを意味します。



ステップ0。最初に無効がありました


何かから始めなければならず、フォームの空の構造のコンパイルを強制することから始めます。


struct { } 

これを行うことはまったく難しくありません。関数を宣言するだけです


 fun struct(init: () -> Unit){ } 

struct(...)関数は、 Unitを返す別の関数をパラメーターとして受け取り、これまでのところ何もしません。 しかし、この関数は、DSLの記述に役立つ重要なKotlinチップを明らかにします。関数の最後の引数が別の関数である場合は、括弧(...)の外側で宣言できます。 関数に引数が1つしかなく、この引数が関数である場合、括弧はまったく省略できます。


したがって、 struct {}コードはstruct({})と同等であり、より短いだけです。


OK、空の構造があります! 実際にはありませんが、何も返さないstruct関数しかありません。 彼女は少なくとも何かを返す必要があります:


 class Struct //  ,      Kotlin fun struc(init: () -> Unit) : Struct { return Struct() } fun main() { val struct = struct { } } 

これで、クラスStructのある種の空のオブジェクトが本当にできました。


ステップ1.その後、データがありました


いくつかのコンテンツを追加します。 ビューを機能させる方法を見つけようとしました


 struct { "field1": 1, "field2": 2 } 

完全に一致させることはできませんでしたが、必要に応じて同時に使用できる3つの代替構文を作成することができました:)


 struct { s("field1" to 1) s("field2" to arrayOf(1, 2, 3)) s("field3" to struct { s("field3.1" to 31) }) }  struct { +{ "field1" to 1 } +{ "field2" to 2 } +{ "field3" to struct { +{ "field3.1" to 31 } } } }  struct( "field1" to 1, "field2" to 2, "field3" to struct( "field1.1" to 11 ) ) 

3番目のケースでは、中括弧ではなく括弧を使用する必要がありましたが、文字数は最も少ないことに注意してください。


それでは、どのようにこの作品を作りますか? まず、Structクラスのデータをどこかに保存する必要があります。 hashMap<String, Any>()を選択しました。 hashMap<String, Any>()は、構造フィールドが文字列であり、値が任意のオブジェクトであるためです。


 class Struct { val children = hashMapOf<String, Any>() } 

次に、このデータを何らかの形で構造に追加する必要があります。 structという単語の後の波括弧内にあるものはすべてstruct(...)引数struct(...)渡した関数であることを思い出させてください。 したがって、 Structオブジェクトを操作するには、渡された関数内でこのオブジェクトにアクセスする必要があります。 そして、私たちはそれをすることができます!


 fun struct(init: Struct.() -> Unit): Struct { val struct = Struct() struct.init() return struct } 

init関数のタイプをStruct.() -> Unit 。 これは、渡される関数がStructクラスの関数またはその拡張関数でなければならないことを意味します。 この関数の宣言により、 struct.init()実行できます。これは、 init()関数内で、たとえばthis介してStructクラスのインスタンスにアクセスできることを意味します。


たとえば、次のようなコードを記述する権利があります。


 struct { this.children.put("field1", 1) // this -   Struct,        struct() } 

これはすでに機能していますが、データ構造記述言語とはあまり似ていません。 設計のサポートを追加する


 struct { +{ "field1" to 1 } } 

"field1" to 1は、 Pair<String, Any>("field1", 1)相当します。 ラムダ関数である中括弧で囲みます。 ラムダ関数の最後の行は、ラムダ関数によって返される値のタイプと値自体を決定します。 つまり、 { "field1" to 1 }は、 Pair<String, Any>を返すラムダです。


ラムダは廃止されましたが、その前の「+」は何ですか? そして、これはオーバーライドされた単項演算子「+」であり、その呼び出しによって、ラムダから受け取ったペアを構造に追加します。 その実装は次のようになります。


 class Struct { val children = hashMapOf<String, Any>() operator fun (() -> Pair<String, Any>).unaryPlus() { //    +   val pair = this.invoke() //      children.put(pair.first, pair.second) //  } } 

次に、フォームの構文のサポートを扱います。


 struct { s("a" to 2) } 

ラムダはありません。すぐにPairオブジェクトとその前にある種のシンボル "s"が作成されます。 実際、「s」も演算子ですが、既に挿入されています。 彼はどこから来たの? だから私は自分で書いた、ここにある:


 class Struct { val children = hashMapOf<String, Any>() infix fun Struct.s(that: Pair<String, Any>): Unit { this.children.put(that.first, that.second) } } 

何も返しませんが、渡されたペアをデータ構造に追加します。 私が選んだ「s」という文字は、演算子の名前は何でも構いません。 ところで、式"field1" to 1は、ペアPair("field1", 1)を返す中置演算子でもあります。


最後に、3番目の構文オプションのサポートを追加します。 最も簡潔ですが、実装の面で最も退屈です。


 struct( "field1" to 1 ) 

"field1" to 1単なるstruct(...)関数の引数であると推測するのは難しくありません。 複数のペアを渡すことができるように、この引数をvararg引数として宣言します


 fun struct(vararg data: Pair<String, Any>, init: Struct.() -> Unit): Struct { val struct = Struct() for (pair in data) { struct.children.put(pair.first, pair.second) } struct.init() return struct } 

ステップ2.そしてDSLを入手しましたか?


構造を説明することを学びましたが、構造を使用する機会を与えなければ、気にする価値はありません。 struct.children.get("field")ようなコードは書きたくありません。 childrenについては何も知りたくありません。 すぐに構造のフィールドに連絡したいと思います。 たとえば、次のようになります: val value = struct["field1"] 。 Structクラスに別のステートメントを定義すると、DSLにこのトリックを教えることができます:)


 class Struct { val children = hashMapOf<String, Any>() operator fun get(s: String): Any? { return children[s] } } 

はい、これは「get」 演算子 (つまり、getterではなくoperator )であり、角括弧を介してオブジェクトにアクセスするときに自動的に呼び出されます。


合計


DSLに成功したと言えます。 各フィールドのタイプを自動的に推測できないという形で明らかな欠陥があるため、理想的ではありませんが、判明しました。 おそらく、しばらく練習すれば、それを改善する方法を見つけることができます。 たぶん、読者はアイデアを持っていますか?


コードサンプル全体は、リンクで表示できます。



Source: https://habr.com/ru/post/J322372/


All Articles