Edited at

Kodein DI のDSLを紐解いてみる。

More than 1 year has passed since last update.


TL;DR

Kodein DIのドキュメント1にはこういうサンプルコードが載っています。


codeInGettingStarted.kt

val kodein = Kodein {

bind<Random>() with provider { SecureRandom() }
bind<Database>() with singleton { SQLiteDatabase() }
}

正直なところ初見では全く読めませんでした。書いてあることはシンプルですが、プログラミング言語には見えません。そこで、Kotlinの文法の勉強も兼ねて内容を整理してみました。

この記事では一旦このコードを省略をなるべくやらない形に置き換えた後、省略のルールを一つ一つ当てはめて元のコードに戻す過程を記載しています。これをネタに文法の理解が進んだら良いかなと思います。Kodein DIの中の仕組みや挙動は扱いません。

識者のマサカリ歓迎します。言語仕様警察どんとこい。間違い探し遊びに付き合ってくれたら幸甚です。


本家のサンプルをそのまま動かす

まずは、サンプルコードがそのまま動くような最小限のコードを作ってみます。 設定して取得してちょっと動かして見るようなものです。


KodeinSample0.kt

package sample0

import org.kodein.di.Kodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import java.security.SecureRandom
import java.util.Random

fun main(args: Array<String>) {
// 設定して取得するコードとして本家のサンプルをコピペ。簡単のためRandomだけにする。
val kodein = Kodein {
bind<Random>() with provider { SecureRandom() }
}

// 設定された SecureRandom() を取得してnextInt()してprintln
val randValue: Random by kodein.instance()
println(randValue.nextInt())
}



省略をしないで同じことをするコードに書き直す

次はこのコードを省略をなるべくやめて古めの言語みたいな書き方で、でも同じ動作になるように書き直してみます。


KodeinSample1.kt

package sample1

import org.kodein.di.Kodein
import org.kodein.di.bindings.NoArgBindingKodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import java.security.SecureRandom
import java.util.Random

// 関数。ただし、NoArgBindingKodein<Any?>に、関数を貼り付ける形。(拡張関数)
fun NoArgBindingKodein<Any?>.setRandom(): SecureRandom {
return SecureRandom()
}

// 同じく拡張関数
fun Kodein.MainBuilder.bindSettings(): Unit {
// thisについているメソッドを呼ぶ。
// this は、MainBuilderのインスタンス。
// bind() も、provider() も、MainBuilderの親クラスないしインターフェースに拡張関数として追加されている。
// setRandom() が、 :: によるCallable Referenceの形でprovider()の引数に渡されている。
this.bind<Random>().with(this.provider(NoArgBindingKodein<Any?>::setRandom))
}

fun main(args: Array<String>) {
// bindSettings() のCallable Referenceが、パラメータに渡されている
val kodein = Kodein.Companion.invoke(false, Kodein.MainBuilder::bindSettings)

// 設定された SecureRandom() を取得してnextInt()してprintln
val randValue: Random by kodein.instance()
println(randValue.nextInt())
}



補足説明

関数 setRandom()は、だいたい普通の関数ですが、NoArgBindingKodein<Any?>クラスに関数を追加する形になっています。こういうものを拡張関数2と呼びます。

関数 bindSettings() も、Kodein.MainBuilder への拡張関数です。中に、thisというものがありますが、Kodein.MainBuilderのインスタンスを意味します。中で呼んでいるbind() も、provider()も、Kodein.MainBuilderの関数です。また、provider()の引数にsetRandom()のCallable Reference3を渡しています。

参考に、Kodein.MainBuilder付近のクラス図4を書いてみました。親クラスなどに関数の定義がある体裁ですね。

classdiagram.png


簡単にしていく


setRandom() を無名関数にしてbindSettings()の中に入れる

setRandom() を無名関数にしてbindSettings()の中に入れてみました。無名関数とするため名前が消えました。無名関数だと関数名考えなくて良いので楽できますね。5


KodeinSample2.kt

package sample2

import org.kodein.di.Kodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import java.security.SecureRandom
import java.util.Random

fun Kodein.MainBuilder.bindSettings(): Unit {
// setRandom()を無名関数にして、providerの引数に入れる
this.bind<Random>().with(this.provider(
fun NoArgBindingKodein<Any?>.(): SecureRandom {
return SecureRandom()
}
))
}

fun main(args: Array<String>) {

val kodein = Kodein.Companion.invoke(false, Kodein.MainBuilder::bindSettings)

// 設定された SecureRandom() を取得してnextInt()してprintln
val randValue: Random by kodein.instance()
println(randValue.nextInt())
}


さて、これからbindSettings()の中身にこれを簡素化する記法を適用していきます。


  • 無名関数をラムダに変更。レシーバの型とか戻り値の型とかreturnは削除


lambda.kt

  this.bind<Random>().with(this.provider({ SecureRandom() }))



  • 最後のパラメータがラムダだった場合は丸括弧の後ろに移動できる。それ以外にパラメータがないので丸括弧は省略できる。これらをprovider()に適用。


moveAfterParentheses.kt

this.bind<Random>().with(this.provider { SecureRandom() })



  • withは中置演算子(infix)なので、ピリオドと丸かっこを削除できる。


withIsInfix.kt

  this.bind<Random>() with this.provider { SecureRandom() }



  • レシーバ付き関数リテラル内なので this は省略可能


removeThis.kt

  bind<Random>() with provider { SecureRandom() }


結果、bindSettings()はこうなりました。


newBindSetting.kt

fun Kodein.MainBuilder.bindSettings(): Unit {

bind<Random>() with provider { SecureRandom() }
}

では次へ行きます。6


bindSettings()を無名関数にして、invokeの中に入れる

bindSettings() を無名関数にしてinvoke()の中に入れてみました。無名関数とするためさっきと同様に名前が消えました。


KodeinSample3.kt

package sample3

import org.kodein.di.Kodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import java.security.SecureRandom
import java.util.Random

fun main(args: Array<String>) {
// bindSettings()を無名関数にして、invokeの引数に入れる
val kodein = Kodein.Companion.invoke(false,
fun Kodein.MainBuilder.(): Unit {
bind<Random>() with provider { SecureRandom() }
}
)

// 設定された SecureRandom() を取得してnextInt()してprintln
val randValue: Random by kodein.instance()
println(randValue.nextInt())
}


さて、これからval kodein へ代入する右辺を簡素化する記法を適用していきます。


  • 無名関数をラムダに変更。レシーバの型とか戻り値の型は削除


lambda2.kt

val kodein = Kodein.Companion.invoke(false, { bind<Random>() with provider { SecureRandom() } })



  • 最後のパラメータがラムダなので丸括弧の外に出す。第1引数はデフォルト値の設定があるので省略。


moveAfterParentheses2.kt

val kodein = Kodein.Companion.invoke { bind<Random>() with provider { SecureRandom() } }



  • 演算子オーバーロードでinvokeというメソッドがあるので、コンパニオンオブジェクトをメソッドのように呼べる。


invokeOperator.kt

  val kodein = Kodein.Companion { bind<Random>() with provider { SecureRandom() } }




  • Companionという名前は省略できる。


omitCompanion.kt

  val kodein = Kodein { bind<Random>() with provider { SecureRandom() } }


ここまでの変形でこの記事の最初に出したソースと同じになりました。色々省略されていましたが、内部的には関数オブジェクトなどをパラメータにして関数を呼びまくっていたということですね。


まとめ

省略記法のために出てきた言語仕様をもう一度まとめてみます。


  • 拡張関数 2

  • Callable Reference3

  • 無名関数 7

  • ラムダ式 8

  • 中置演算子 9

  • コンパニオンオブジェクト10

  • オペレーターオーバーロードでのinvoke 11

  • 最後のパラメータがラムダのときは丸括弧の後ろに移動して良い。12

  • デフォルト引数が設定されていたら省略して良い。13

この辺の文法を押さえておけば、今後どんなDSLを見てもついて行けそうな気がしますね!仕組みがわかってしまえばどうってことはありません。14


おまけ:値の取得の by ってなんですか?

値の取得の方の話です。サンプルでは、値の取得はLocal Delegated Properties15でやっていますが、直接KodeinのAPIから取ることもできます。16 つまり、以下のコードは同じ動作をします。

元々のコード


retrieveWithLocalDelegatedProperties.kt

  // 設定された SecureRandom() を取得してnextInt()してprintln

val randValue: Random by kodein.instance()
println(randValue.nextInt())

直接取得するコード


retrieveWithDirectRetrieval.kt

  // by というLocal Delegated Properties ではなく、KodeinのAPIから直接値を取得できる。

val randValue: Random = kodein.direct.instance()
println(randValue.nextInt())

機能的なご利益があるのでプロパティで取得しましょうというのが第一のようです。でも直接取ることもできますよと。17


おまけ2:どうでもいいポエム

コードには説明用のコメントをいれるというよくあるスタイルでやりましたが、あなたが実際に書いて本番で動かすコードには文法を説明するコメントを入れたりしないでくださいね! 18


バージョン番号など


  • Kotlin 1.2.70

  • Kodein DI 5.2.0





  1. http://kodein.org/Kodein-DI/?5.2/getting-started#_declaration 



  2. https://kotlinlang.org/docs/reference/extensions.html#extension-functions 



  3. https://kotlinlang.org/docs/reference/reflection.html#callable-references 



  4. http://plantuml.com/ で描きました。 



  5. 名前って規約とか設計とかポリシーとか色々あるじゃないですか。 



  6. なんだか数学の授業で式変形をどんどんやっているみたいですね!説明の変形が飛びすぎると聞く方はついていけなくなるんですよね。 



  7. https://kotlinlang.org/docs/reference/lambdas.html#anonymous-functions 



  8. https://kotlinlang.org/docs/reference/lambdas.html 



  9. https://kotlinlang.org/docs/reference/functions.html#infix-notation 



  10. https://kotlinlang.org/docs/reference/object-declarations.html#companion-objects 



  11. https://kotlinlang.org/docs/reference/operator-overloading.html#invoke 



  12. https://kotlinlang.org/docs/reference/lambdas.html#passing-a-lambda-to-the-last-parameter 



  13. https://kotlinlang.org/docs/reference/functions.html#default-arguments 



  14. 「やり方さえ分かっちゃえば簡単なもんだね」 



  15. https://kotlinlang.org/docs/reference/delegated-properties.html#local-delegated-properties-since-11 



  16. https://kodein.org/Kodein-DI/?5.2/core#direct-retrieval 



  17. これはDSLではないのでおまけ扱いにしました。 



  18. コンストラクタのjavadocコメントに、コンストラクタ って書く人がいるんですよね。