Kotlin DSLを理解してみる / Understanding Kotlin DSL

Bbf2a0e77c98104ac16ef6d0098c1385?s=47 yagi2
September 18, 2018
950

Kotlin DSLを理解してみる / Understanding Kotlin DSL

集まれKotlin好き!Kotlin愛好会 vol4 - 2018-09-20

slide repo : yagi2/love-kotlin-2018-09-20: LT slide @ Kotlin愛好会 vol4 2018-09-20 - https://github.com/yagi2/love-kotlin-2018-09-20

links:
Kotlin/kotlinx.html: Kotlin DSL for HTML - https://github.com/Kotlin/kotlinx.html

ngsw-taro/knit: JUnit API set for Kotlin - https://github.com/ngsw-taro/knit/

Operator overloading - Kotlin Programming Language - https://kotlinlang.org/docs/reference/operator-overloading.html

KEEP/scope-control-for-implicit-receivers.md at master · Kotlin/KEEP - https://github.com/Kotlin/KEEP/blob/master/proposals/scope-control-for-implicit-receivers.md

kotlinx.html/DSL-markers.md at dsl-markers · Kotlin/kotlinx.html - https://github.com/Kotlin/kotlinx.html/blob/dsl-markers/DSL-markers.md

Kotlin 1.1-M03 is here! | Kotlin Blog - https://blog.jetbrains.com/kotlin/2016/11/kotlin-1-1-m03-is-here/

Kotlin/anko: Pleasant Android application development - https://github.com/Kotlin/anko

yagi2/LayoutDSLSample: Let's implement DSL that create layout in Android with Kotlin! - https://github.com/yagi2/LayoutDSLSample

Kotlinイン・アクション | Dmitry Jemerov, Svetlana Isakova, 長澤 太郎, 藤原 聖, 山本 純平, yy_yank |本 | 通販 | Amazon - https://www.amazon.co.jp/dp/4839961743/

Bbf2a0e77c98104ac16ef6d0098c1385?s=128

yagi2

September 18, 2018
Tweet

Transcript

  1. Kotlin DSL を理解してみる Kotlin DSL を理解してみる Kotlin DSL を理解してみる 青柳

    樹 (Itsuki AOYAGI) - シーサー株式会社 Kotlin 愛好会 Vol4 2018-09-20 1
  2. 自己紹介 自己紹介 自己紹介  青柳 樹(Ituki AOYAGI )  やぎにい(@yagi2,

    @yaginier)  Android エンジニア @ シーサー株式会社  普段は京都に居ます⛩  Kotlin 歴:1 年と半年弱 2
  3. おしながき おしながき おしながき  DSL とはなんぞや  Kotlin でDSL を作る時によく使う言語機能

     実際にDSL を作ってみる 3
  4. DSL DSL DSL Domain Speci c Language Domain Speci c

    Language (ドメイン固有言語) (ドメイン固有言語) 特定の領域・分野の問題を解決するための言語。 身近なところだと  SQL  正規表現  make 4 . 1
  5. 内部と外部 内部と外部 内部DSL 内部DSL アプリケーションから独立しているもの 独自に構文解析を行う必要がある。 外部DSL 外部DSL アプリケーション内で その言語を特定のドメイン向けに使う

    以降の"DSL" は内部DSL について喋っていきます 4 . 2
  6. Kotlin でDSL を作るメリット Kotlin でDSL を作るメリット  ✨ 静的型付き言語なので コンパイル時に構文チェックが行われる✨

     ✨IDE での補完が強い✨  ✨Kotlin の言語機能が強い✨ 4 . 3
  7. Kotlin DSL Example Kotlin DSL Example DSL は構造と文法を持つ。 普通に書いたケース val

    html = createHtml() val table = createTable() html.addChild(table) val tr = createTr() table.addChild(tr) val td = createTd() tr.addChild(td) td.text = " " 4 . 4
  8. Kotlin DSL Example Kotlin DSL Example DSL は構造と文法を持つ。 DSL を用いたケース

    ぱっと見の圧倒的わかりやすさ val html = createHtml(). table { tr { td { + " " } } } Kotlin/kotlinx.html: Kotlin DSL for HTML 4 . 5
  9. DSL 作成時良く使う言語機能 DSL 作成時良く使う言語機能 DSL 作成時良く使う言語機能  拡張関数・拡張プロパティ  メソッドの引数の最後がラムダ式であれば、外側に出せる

     レシーバ指定のラムダ式  中置関数  演算子オーバーロード  アノテーション 5 . 1
  10. 拡張関数・拡張プロパティ 拡張関数・拡張プロパティ 既存のクラスに新しいメソッドやプロパティを生やすことが出来る。 fun String.toPackedString() = this.replace("\n", "").replace(" ", "")

    """ { "name": "yagi2", "age": 24 } """.toPackedString() // -> "{"name":"yagi2","age":24}" var <T> List<T>.lastIndex: Int get() = size - 1 listOf(1, 2, 3).lastIndex // -> 2 5 . 2
  11. メソッドの引数の最後がラムダ式であれば メソッドの引数の最後がラムダ式であれば 外側に出せる 外側に出せる ちょっとDSL っぽい見た目になる。 fun hoge(num: Int, lambd:

    () -> String) hoge(100, { "this is lambda" }) hoge(100) { "this is lambda" } fun piyo(lambda: () -> String) piyo({ "this is lambda "}) piyo { "this is lambda" } 5 . 3
  12. レシーバ指定のラムダ式 レシーバ指定のラムダ式 ラムダ式にレシーバを指定して、ラムダ式の中のスコープの をそのクラ スにする機能。 data class Fruit(val name: String,

    val num: Int) fun hoge(lambda: Fruit.() -> Unit) { val apple = Fruit(" ", 10) apple.lambda() } hoge { // this Fruit println("${this.name} ${this.num}個 ") // -> 10個 // this 省略可能 println("${name} ${num}個 ") // -> 10個 } 5 . 4
  13. レシーバ指定のラムダ式 レシーバ指定のラムダ式 スコープの関数のwith 第1 引数でT クラスを受け取り、そのT クラスを指定したラムダ式を第2 引数 で受け取る。 fun

    <T, R> with(receiver: T, block: T.() -> R): R { ... } data class Fruit(val name: String, val num: Int) val apple = Fruit(" ", 10) with(apple) { // apple(Fruit ) 渡 this Fruit println("${name} ${num}個欲 ") // -> 10個欲 } 5 . 5
  14. 中置関数 中置関数 メソッド呼び出し時に を使わずにスペースで挟むこと(中置にすること) で呼び出せるようになる。 をつけたメソッドが中置関数となる。 これは に実装がある "a" to

    "b" // -> Pair<String, String> public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that) 5 . 6
  15. 中置関数 中置関数 長澤太郎さんが作られているJUnit をシンプルに書けるライブラリ での例 が中置関数になっている。 ngsw-taro/knit: JUnit API set

    for Kotlin val actual = "hoge" val expected = "hoge" actual.should be expected interface Asserter<T> { infix fun be(expected: T) } internal class AsserterImpl<T>(override val target: T) : Asserter<T> { override fun be(expected T) { target.should(`is`(expected)) } } 5 . 7
  16. 演算子オーバーロード 演算子オーバーロード Kotlin では既に用意されている数種類のオペレーターをオーバーロードして 実装することができる。    Operator overloading - Kotlin

    Programming Language data class Point(val x: Int, val y: Int) operator fun Point.plus(that: Point) = Point(this.x + that.x, this.y + that.y) val p1 = Point(10, 10) val p2 = Point(15, 20) p1 + p2 // -> Point(25, 30) // OK p1.plus(p2) // -> Point(25, 30) 5 . 8
  17. アノテーション アノテーション プロックが入れ子になった際に、内側のブロックから外側のブロックにアク セスできるようになってしまうのが多々ある。 それを防ぐためのアノテーション。 具体的には以下のようなケースを防ぐことが出来る。   html { head

    { // head 中 this this@html this@head head { } } } KEEP/scope-control-for-implicit-receivers.md at master · Kotlin/KEEP kotlinx.html/DSL-markers.md at dsl-markers · Kotlin/kotlinx.html Kotlin 1.1-M03 is here! | Kotlin Blog 5 . 9
  18. Kotlin Gradle DSL Kotlin Gradle DSL Kotlin Gradle DSL での

    の書き方 それぞれの実装を見てみる dependencies.implementation("androidx.constraintlayout:constraintlayout:1.1.3") dependencies { implementation("androidx.constraintlayout:constraintlayout:1.1.3") } 6 . 1
  19. 拡張関数のケース 拡張関数のケース は クラスを返す その クラスに対して、拡張関数が生えている dependencies.implementation("androidx.constraintlayout:constraintlayout:1.1.3") fun DependencyHandler.`implementation`(dependencyNotation: Any):

    Dependency = add("implementation", dependencyNotation) 6 . 2
  20. レシーバ指定のラムダ式をとるケース レシーバ指定のラムダ式をとるケース の実態は クラスを指定したラムダ式を取っているため は を継承しているので、そ の中で呼んでる はさっきの拡張関数。 dependencies {

    implementation("androidx.constraintlayout:constraintlayout:1.1.3") } class DependencyHandlerScope(val dependencies: DependencyHandler) : DependencyHandler by depen fun Project.dependencies(configuration: DependencyHandlerScope.() -> Unit) = DependencyHandlerScope(dependencies).configuration() } dependencies { // this DependencyHandlerScope } 6 . 3
  21. Anko という、Android アプリを制作を快適にするライブラリがある その中に というDSL でレイアウトを組めるものがある linearLayout { padding =

    dip(30) textView { text = "A" }.lparams { margin = dip(30) } } Kotlin/anko: Pleasant Android application development 7 . 1
  22. 実際に作ってみる 実際に作ってみる Anko のようなDSL でレイアウトをビルドするようなものを作ってみる (とりあえず汎用性は無視) (時間がありそうならライブコーディングで作ります) 目指す完成形 と を作れて

    も指定できるように linearLayout { textView { text = "Hello,Kotlin DSL!" textColor = Color.RED }.lparams { margin = dip(10) } } 7 . 2
  23. Layout を作るDSL をつくる Layout を作るDSL をつくる 外側から作っていく。 linearLayout { textView

    { text = "Hello,Kotlin DSL!" textColor = Color.RED }.lparams { margin = dip(10) } } 8 . 1
  24. LinearLayout LinearLayout 一番外側 を作る これでここまで書けるようになった fun linearLayout(init: () -> Unit):

    LinearLayout = LinearLayout(context) linearLayout { } 8 . 2
  25. TextView TextView 次に を同じように作っていく これでここまで書けるようになった fun textView(init: () -> Unit):

    TextView = TextView(context) linearLayout { textView { } } 8 . 3
  26. TextView TextView の中で を書きたい → レシーバ指定で のラムダの中の を にする これでここまで書けるようになった

    fun textView(init: TextView.() -> Unit): TextView = TextView(context).apply(init) linearLayout { textView { text = "Hello,Kotlin DSL!" setColor(Color.RED) } } 8 . 4
  27. 親View への追加 親View への追加 作った を親( ) に追加したい → メソッドを

    の拡張関数にする fun LinearLayout.textView(init: TextView.() -> Unit): TextView { val textView = TextView(context).apply(init) addView(textView) // this LinearLayout return textView } 8 . 5
  28. 親View への追加 親View への追加 メソッドを の拡張関数にしたことで の中で メソッドが呼べなくなった → の中の

    を にする これでここまで書いて、レイアウトを表示することができるようになった fun linearLayout(init: LinearLayout.() -> Unit): LinearLayout = LinearLayout(context).apply(init) linearLayout { textView { text = "Hello,Kotlin DSL!" setColor(Color.RED) } } 8 . 6
  29. LayoutParams の適用 LayoutParams の適用 メソッドを実装していく これでここまで書けるようになった fun TextView.lparams(init: () ->

    Unit): TextView { val lp = LinearLayout.LayoutParams(width, height) layoutParams = lp // this.layoutParams return this } linearLayout { textView { text = "Hello,Kotlin DSL!" setColor(Color.RED) }.lparams { } } 8 . 7
  30. LayoutParams の適用 LayoutParams の適用 のラムダ内で などをセットしたい → レシーバ指定してあげる これでここまで書けるようになった fun

    TextView.lparams(init: LinearLayout.LayoutParams.() -> Unit): TextView { val lp = LinearLayout.LayoutParams(width, height) layoutParams = lp // this.layoutParams return this } linearLayout { textView { text = "Hello,Kotlin DSL!" setColor(Color.RED) }.lparams { setMargins(dip(10)) } } 8 . 8
  31. LayoutParams の適用 LayoutParams の適用 に と を渡せるようにする(0 のため) fun TextView.lparams(

    width: Int, height: Int, init: LinearLayout.LayoutParams.() -> Unit ): TextView { val lp = LinearLayout.LayoutParams(width, height) layoutParams = lp // this.layoutParams return this } 8 . 9
  32. LayoutParams の適用 LayoutParams の適用 このままだと という感じになるので、デフォルト値を与える そして引数のラムダを適用する lparams(WRAP_CONTENT, WRAP_CONTENT) {

    setMargin(dip(10)) } fun TextView.lparams( width: Int = LinearLayout.LayoutParams.WRAP_CONTENT, height: Int = LinearLayout.LayoutParams.WRAP_CONTENT, init: LinearLayout.LayoutParams.() -> Unit ): TextView { val lp = LinearLayout.LayoutParams(width, height).apply(init) layoutParams = lp // this.layoutParams return this } 8 . 10
  33. 9 割完成 9 割完成 今までの実装で以下のように書いて表示することができるようになった , …… ? linearLayout {

    textView { text = "Hello,Kotlin DSL!" setColor(Color.RED) }.lparams { setMargins(dip(10)) } } 8 . 11
  34. プロパティのセット プロパティのセット , を , で書きたい 理想の完成形 linearLayout { textView

    { text = "Hello,Kotlin DSL!" color = Color.RED }.lparams { margin = dip(10) } } 8 . 12
  35. setColor の実装 setColor の実装 プロパティっぽくするならプロパティにするしかない → 拡張プロパティで解決 これでここまで書けるようになった var TextView.textColor:

    Int get() = currentTextColor set(value) = setTextColor(value) linearLayout { textView { text = "Hello,Kotlin DSL!" color = Color.RED }.lparams { setMargins(dip(10)) } } 8 . 13
  36. setMargins の実装 setMargins の実装 も同じようにしていく …… var LinearLayout.LayoutParams.margin: Int set(value)

    = setMargins(value, value, value, value) 8 . 14
  37. setMargins の実装 setMargins の実装 は4 方向にセットするメソッドであるが 当然だが は存在しない → 例外を投げよう!

    と書けてしまう(実行時には落ちるが) いや、不親切すぎでは…… var LinearLayout.LayoutParams.margin: Int get() = throw RuntimeException("This property does not have a getter.") set(value) = setMargins(value, value, value, value) .lparams { val currentMargin = margin } 8 . 15
  38. setMargins の実装 setMargins の実装 じゃあビルドできないようにしてあげよう → タグを レベルでつけてあげる 親切になった☺ var

    LinearLayout.LayoutParams.margin: Int @Deprecated("This propetry does not have a getter." level = DeprecationLevel.ERROR) get() = throw RuntimeException("This property does not have a getter.") set(value) = setMargins(value, value, value, value) .lparams { // val currentMargin = margin } 8 . 16
  39. 完成 完成 これで最初の完成形が動いて、レイアウトが表示できるようになった linearLayout { textView { text = "Hello,Kotlin

    DSL!" textColor = Color.RED }.lparams { margin = dip(10) } // 側 組 書 (1..10).forEach { textView { text = it.toString() } } } 8 . 17
  40. 完成 完成 DSL を作るときのコツ  そのラムダ式の中の は何になっているのか  外側から作っていく 今回作ったDSL

    のリポジトリ ブランチが実装前で完成形のDSL が存在する ブランチに実装が存在するので穴開きクイズで実装してみよう! yagi2/LayoutDSLSample 8 . 18
  41. おしまい おしまい おしまい Have a nice Kotlin! Have a nice

    Kotlin! DSL を作るときのリファレンスと このスライドのリポジトリ Type-Safe Builders - Kotlin Programming Language HTML Builder | Try Kotlin yagi2/love-kotlin-2018-09-20 9 . 1
  42. Kotlin DSL をもっと知るには Kotlin DSL をもっと知るには Kotlin イン・アクションの11 章がDSL について詳しく書かれている

    Kotlin イン・アクション | Dmitry Jemerov, Svetlana Isakova, 長澤 太郎, 藤原 聖, 山本 純平, yy_yank | 本 | 通販 | Amazon 9 . 2