Slide 1

Slide 1 text

Kotlin DSL を理解してみる Kotlin DSL を理解してみる Kotlin DSL を理解してみる 青柳 樹 (Itsuki AOYAGI) - シーサー株式会社 Kotlin 愛好会 Vol4 2018-09-20 1

Slide 2

Slide 2 text

自己紹介 自己紹介 自己紹介  青柳 樹(Ituki AOYAGI )  やぎにい(@yagi2, @yaginier)  Android エンジニア @ シーサー株式会社  普段は京都に居ます⛩  Kotlin 歴:1 年と半年弱 2

Slide 3

Slide 3 text

おしながき おしながき おしながき  DSL とはなんぞや  Kotlin でDSL を作る時によく使う言語機能  実際にDSL を作ってみる 3

Slide 4

Slide 4 text

DSL DSL DSL Domain Speci c Language Domain Speci c Language (ドメイン固有言語) (ドメイン固有言語) 特定の領域・分野の問題を解決するための言語。 身近なところだと  SQL  正規表現  make 4 . 1

Slide 5

Slide 5 text

内部と外部 内部と外部 内部DSL 内部DSL アプリケーションから独立しているもの 独自に構文解析を行う必要がある。 外部DSL 外部DSL アプリケーション内で その言語を特定のドメイン向けに使う 以降の"DSL" は内部DSL について喋っていきます 4 . 2

Slide 6

Slide 6 text

Kotlin でDSL を作るメリット Kotlin でDSL を作るメリット  ✨ 静的型付き言語なので コンパイル時に構文チェックが行われる✨  ✨IDE での補完が強い✨  ✨Kotlin の言語機能が強い✨ 4 . 3

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Kotlin DSL Example Kotlin DSL Example DSL は構造と文法を持つ。 DSL を用いたケース ぱっと見の圧倒的わかりやすさ val html = createHtml(). table { tr { td { + " " } } } Kotlin/kotlinx.html: Kotlin DSL for HTML 4 . 5

Slide 9

Slide 9 text

DSL 作成時良く使う言語機能 DSL 作成時良く使う言語機能 DSL 作成時良く使う言語機能  拡張関数・拡張プロパティ  メソッドの引数の最後がラムダ式であれば、外側に出せる  レシーバ指定のラムダ式  中置関数  演算子オーバーロード  アノテーション 5 . 1

Slide 10

Slide 10 text

拡張関数・拡張プロパティ 拡張関数・拡張プロパティ 既存のクラスに新しいメソッドやプロパティを生やすことが出来る。 fun String.toPackedString() = this.replace("\n", "").replace(" ", "") """ { "name": "yagi2", "age": 24 } """.toPackedString() // -> "{"name":"yagi2","age":24}" var List.lastIndex: Int get() = size - 1 listOf(1, 2, 3).lastIndex // -> 2 5 . 2

Slide 11

Slide 11 text

メソッドの引数の最後がラムダ式であれば メソッドの引数の最後がラムダ式であれば 外側に出せる 外側に出せる ちょっと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

Slide 12

Slide 12 text

レシーバ指定のラムダ式 レシーバ指定のラムダ式 ラムダ式にレシーバを指定して、ラムダ式の中のスコープの をそのクラ スにする機能。 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

Slide 13

Slide 13 text

レシーバ指定のラムダ式 レシーバ指定のラムダ式 スコープの関数のwith 第1 引数でT クラスを受け取り、そのT クラスを指定したラムダ式を第2 引数 で受け取る。 fun 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

Slide 14

Slide 14 text

中置関数 中置関数 メソッド呼び出し時に を使わずにスペースで挟むこと(中置にすること) で呼び出せるようになる。 をつけたメソッドが中置関数となる。 これは に実装がある "a" to "b" // -> Pair public infix fun A.to(that: B): Pair = Pair(this, that) 5 . 6

Slide 15

Slide 15 text

中置関数 中置関数 長澤太郎さんが作られているJUnit をシンプルに書けるライブラリ での例 が中置関数になっている。 ngsw-taro/knit: JUnit API set for Kotlin val actual = "hoge" val expected = "hoge" actual.should be expected interface Asserter { infix fun be(expected: T) } internal class AsserterImpl(override val target: T) : Asserter { override fun be(expected T) { target.should(`is`(expected)) } } 5 . 7

Slide 16

Slide 16 text

演算子オーバーロード 演算子オーバーロード 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

Slide 17

Slide 17 text

アノテーション アノテーション プロックが入れ子になった際に、内側のブロックから外側のブロックにアク セスできるようになってしまうのが多々ある。 それを防ぐためのアノテーション。 具体的には以下のようなケースを防ぐことが出来る。   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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

拡張関数のケース 拡張関数のケース は クラスを返す その クラスに対して、拡張関数が生えている dependencies.implementation("androidx.constraintlayout:constraintlayout:1.1.3") fun DependencyHandler.`implementation`(dependencyNotation: Any): Dependency = add("implementation", dependencyNotation) 6 . 2

Slide 20

Slide 20 text

レシーバ指定のラムダ式をとるケース レシーバ指定のラムダ式をとるケース の実態は クラスを指定したラムダ式を取っているため は を継承しているので、そ の中で呼んでる はさっきの拡張関数。 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

Slide 21

Slide 21 text

Anko という、Android アプリを制作を快適にするライブラリがある その中に というDSL でレイアウトを組めるものがある linearLayout { padding = dip(30) textView { text = "A" }.lparams { margin = dip(30) } } Kotlin/anko: Pleasant Android application development 7 . 1

Slide 22

Slide 22 text

実際に作ってみる 実際に作ってみる Anko のようなDSL でレイアウトをビルドするようなものを作ってみる (とりあえず汎用性は無視) (時間がありそうならライブコーディングで作ります) 目指す完成形 と を作れて も指定できるように linearLayout { textView { text = "Hello,Kotlin DSL!" textColor = Color.RED }.lparams { margin = dip(10) } } 7 . 2

Slide 23

Slide 23 text

Layout を作るDSL をつくる Layout を作るDSL をつくる 外側から作っていく。 linearLayout { textView { text = "Hello,Kotlin DSL!" textColor = Color.RED }.lparams { margin = dip(10) } } 8 . 1

Slide 24

Slide 24 text

LinearLayout LinearLayout 一番外側 を作る これでここまで書けるようになった fun linearLayout(init: () -> Unit): LinearLayout = LinearLayout(context) linearLayout { } 8 . 2

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

TextView TextView の中で を書きたい → レシーバ指定で のラムダの中の を にする これでここまで書けるようになった fun textView(init: TextView.() -> Unit): TextView = TextView(context).apply(init) linearLayout { textView { text = "Hello,Kotlin DSL!" setColor(Color.RED) } } 8 . 4

Slide 27

Slide 27 text

親View への追加 親View への追加 作った を親( ) に追加したい → メソッドを の拡張関数にする fun LinearLayout.textView(init: TextView.() -> Unit): TextView { val textView = TextView(context).apply(init) addView(textView) // this LinearLayout return textView } 8 . 5

Slide 28

Slide 28 text

親View への追加 親View への追加 メソッドを の拡張関数にしたことで の中で メソッドが呼べなくなった → の中の を にする これでここまで書いて、レイアウトを表示することができるようになった fun linearLayout(init: LinearLayout.() -> Unit): LinearLayout = LinearLayout(context).apply(init) linearLayout { textView { text = "Hello,Kotlin DSL!" setColor(Color.RED) } } 8 . 6

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

9 割完成 9 割完成 今までの実装で以下のように書いて表示することができるようになった , …… ? linearLayout { textView { text = "Hello,Kotlin DSL!" setColor(Color.RED) }.lparams { setMargins(dip(10)) } } 8 . 11

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

setMargins の実装 setMargins の実装 も同じようにしていく …… var LinearLayout.LayoutParams.margin: Int set(value) = setMargins(value, value, value, value) 8 . 14

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

完成 完成 これで最初の完成形が動いて、レイアウトが表示できるようになった linearLayout { textView { text = "Hello,Kotlin DSL!" textColor = Color.RED }.lparams { margin = dip(10) } // 側 組 書 (1..10).forEach { textView { text = it.toString() } } } 8 . 17

Slide 40

Slide 40 text

完成 完成 DSL を作るときのコツ  そのラムダ式の中の は何になっているのか  外側から作っていく 今回作ったDSL のリポジトリ ブランチが実装前で完成形のDSL が存在する ブランチに実装が存在するので穴開きクイズで実装してみよう! yagi2/LayoutDSLSample 8 . 18

Slide 41

Slide 41 text

おしまい おしまい おしまい 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

Slide 42

Slide 42 text

Kotlin DSL をもっと知るには Kotlin DSL をもっと知るには Kotlin イン・アクションの11 章がDSL について詳しく書かれている Kotlin イン・アクション | Dmitry Jemerov, Svetlana Isakova, 長澤 太郎, 藤原 聖, 山本 純平, yy_yank | 本 | 通販 | Amazon 9 . 2