Upgrade to Pro — share decks privately, control downloads, hide ads and more …

KotlinでDSLを作る #Kotlin_Sansan

C057726b79d9eaed10c136794ef833fa?s=47 Jumpei Yamamoto
November 09, 2017
970

KotlinでDSLを作る #Kotlin_Sansan

C057726b79d9eaed10c136794ef833fa?s=128

Jumpei Yamamoto

November 09, 2017
Tweet

Transcript

  1. KotlinでDSLを作る 11/09 Kotlin勉強会 at Sansan ⼭本純平

  2. Kotlin イン・アクション 2 JetBrainsのKotlinメンバによる Kotlin解説本 今⽇はその中から 「第11章 DSLの作成」 の内容を紹介!

  3. ⾃⼰紹介 3 ⼭本純平 twitter: @boohbah github: https://github.com/yamamotoj Sansan株式会社 DSOC事業部 R&Dグループ

    データ分析 Eight Android版の開発
  4. ⾃⼰紹介 - Kotlin⼊⾨までの助⾛読本 - Kotlinイン・アクション 4

  5. Agenda - ドメイン固有⾔語のコンセプト - 内部DSLと外部DSL - DSL作成のためのKotlinの機能 - 中置関数 -

    invoke関数 - レシーバー付きラムダ 5
  6. ライブラリのAPIとして洗練されているのは? - コードで何が置きているかが明確になっていること。 - 良い名前と、良いコンセプト - 不必要な記述がなく、コードがきれいに⾒えること - これを達成する⼿段がDSL -

    洗練されたAPIは⾔語に組み込みの機能と区別がつかない?
  7. ドメイン固有⾔語(Domain Specific Language)とは? - ⇔ 汎⽤的なプログラム⾔語 - 特定のタスクやドメインに焦点を当て、それとは関係な い機能を削ぎ落としたプログラム⾔語 -

    外部DSLと内部DSLがある 7
  8. 外部DSL - 動作するアプリケーションとは別の独⽴した⾔語 - 独⾃に字句、構⽂解析を⾏い、必要な機能を実装する - 特定の分野に最適化される反⾯、ホストのプログラムとのやりとりが煩雑 8 # SQL

    SELECT * FROM user_table WHERE name = ’hoge’; # 正規表現 http://[¥w¥d/%#$&?()~_.=+-]+
  9. 内部DSL - アプリケーションの汎⽤⾔語を、特定のドメイン向けにそれっぽく使う - 同じ⾔語で記述されているので、やり取りもスムーズ - 表現能⼒(== 読みやすさ)は元の⾔語に依存する - 静的型付⾔語の場合、コンパイル時に構⽂のチェックされる

    9
  10. 静的型付⾔語でDSLを作る上でのポイント - 適切にドメインの語彙を使⽤しているか? - いかに読み⼿が(余計な複雑さなしに)⾃然にその⾔語を読み下せるか? - 型付けによっていかに適切にIDEの補完を効かせることができるか? 10

  11. DSLは構造と⽂法をもつ val html = createHTML() val table = createTable() html.addChild(table)

    val tr = createTR() table.addChild(tr) val td = createTD() tr.addChild(td) td.text = ”cell” 11 val html = createHTML(). table { tr{ td { + “cell” } } }
  12. Kotlinで内部DSLの作成

  13. DSL作成のために使えるKotlinの機能 - レシーバー付きラムダ - invoke規約 - 中置関数 - 演算⼦のオーバーロード -

    getメソッドの規約 - 拡張関数 - メンバ拡張 13 - 括弧の外側のラムダ引数 - 委譲プロパティ - 委譲クラス - 静的型付け - @DslMakerアノテーション
  14. DSLでよく使われるKotlin機能ランキング

  15. DSLでよく使われるKotlin機能ランキング 第三位

  16. DSLでよく使われるKotlin機能ランキング 第三位 中置関数

  17. 中置関数とは? 17 // 関数定義 infix fun <A, B> A.to(that: B)

    = Pair(this, that) // Pairを⽣成する 1.to(“one”) // 通常の呼び出し 1 to “one” // 中置呼び出し 関数の宣⾔にinfixとつけることで、ドットや()なしで関数を呼び出せる
  18. 中置呼び出しのDSLへの適⽤ 18 kotlintestライブラリより s should startWith(“kot”)

  19. 中置呼び出しのDSLへの適⽤ 19 s should startWith(“kot”) infix fun <T> T.should( matcher:

    Matcher<T>) = matcher.test(this) shouldを中置関数として定義
  20. 中置呼び出しのDSLへの適⽤ 20 s should startWith(“kot”) infix fun <T> T.should( matcher:

    Matcher<T>) = matcher.test(this) interface Matcher<T>{ fun test(value: T) } Matcher: 値のアサーションを⾏うためのinterface
  21. 中置呼び出しのDSLへの適⽤ 21 s should startWith(“kot”) class startWith(val prefix: String) :

    Matcher<String> { override fun test(value: String) { if (!value.startWith(prefix)) throw AssertionError(“error”) } } startWithはMatcherインターフェイスを実装したクラス
  22. 中置呼び出しのDSLへの適⽤ 22 s should startWith(“kot”) Matcher interfaceを引数にとる中置関数

  23. 中置呼び出しのDSLへの適⽤ 23 s should startWith(“kot”) Matcher interfaceを引数にとる中置関数 Matcher interfaceを実装したクラス

  24. さらに、こんな書き⽅も 24 s should start with “kot”

  25. さらに、こんな書き⽅も 25 s should start with “kot” shouldはStringの拡張関数で中置関数 infix fun

    String.should(x: start) = StartWrapper(this)
  26. さらに、こんな書き⽅も 26 s should start with “kot” infix fun String.should(x:

    start) = StartWrapper(this) object start startはobjectとして宣⾔ object: シングルトンのクラス宣⾔
  27. さらに、こんな書き⽅も 27 s should start with “kot” class StartWrapper(val value:

    String) { infix fun with(prefix: String) = if (!value.startsWith(prefix)) throw AssertionError(“error”) } withはshouldで返されたStartWrapperクラスのメソッド
  28. さらに、こんな書き⽅も 28 s should start with “kot” startオブジェクトを引数にとり StartWrapperクラスを返す中置関数

  29. さらに、こんな書き⽅も 29 s should start with “kot” startオブジェクトを引数にとり StartWrapperクラスを返す中置関数 StartWrapperクラスのメソッド

    中置関数
  30. ポイントは object 宣⾔ 30 s should start with “kot” infix

    fun String.should(x: start) = StartWrapper(this) object start objectは通常⼤⽂字で始まるが、ここでは⼩⽂字で開始される
  31. ポイントは object 宣⾔ 31 s should end with “lin” infix

    fun String.should(x: end) = EndWrapper(this) object end startの代わりにendというobjectも定義可能 should関数も、endを引数にとるオーバーロード
  32. shouldまでだとどのオーバーロードが使われるか決定さ れていない infix fun <T> T.should( matcher: Matcher<T>) = matcher.test()

    infix fun String.should(x: end) = EndWrapper(this) infix fun String.should(x: start) = StartWrapper(this) s should
  33. ポイントは object 宣⾔ infix fun <T> T.should( matcher: Matcher<T>) =

    matcher.test() infix fun String.should(x: end) = EndWrapper(this) infix fun String.should(x: start) = StartWrapper(this) s should startWith(“kot”)
  34. ポイントは object 宣⾔ infix fun <T> T.should( matcher: Matcher<T>) =

    matcher.test() infix fun String.should(x: end) = EndWrapper(this) infix fun String.should(x: start) = StartWrapper(this) s should start with “kot”
  35. ポイントは object 宣⾔ infix fun <T> T.should( matcher: Matcher<T>) =

    matcher.test() infix fun String.should(x: end) = EndWrapper(this) infix fun String.should(x: start) = StartWrapper(this) s should end with “lin”
  36. DSLでよく使われるKotlin機能ランキング 第⼆位

  37. DSLでよく使われるKotlin機能ランキング 第⼆位 invoke規約

  38. Kotlinの規約 - operator修飾⼦をつけて関数(拡張関数)を定義すると、対応す る演算⼦を呼び出したときのその関数に変換される 38 class Foo(val value: Int) operator

    fun Foo.plus(that: Foo) = this.value + that.value Foo(1) + Foo(2) // => Foo(3) plus関数が「+」演算⼦に対応
  39. いろいろな規約 39 演算⼦ 対応する関数 a - b minus a *

    b times a / b div ++ a inc a == b equals >, <, =>, =< compareTo a[index] get val (a, b) = p componentN
  40. invoke規約 40 class Foo operator fun invoke() = print(“foo”) operator

    fun invoke(s: String) = print(s) val foo = Foo() foo() // foo foo(“hoge”) // hoge ()をつかった呼び出しに対する規約。invoke関数が対応する
  41. invoke規約 41 operator fun Foo.invoke(f: ()->Unit) = f() val foo

    = Foo() foo.invoke({ }) foo({ }) foo{ }
  42. gradle Kotlin DSLの例 42 dependencies.compile("junit:junit:4.11") dependencies { compile("junit:junit:4.11") } この2つを同時に実現したい

  43. 通常の呼び出し 43 class DependencyHandler { fun compile(coordinate: String) { println("Added

    dependency on $coordinate") } } dependencies.compile("junit:junit:4.11")
  44. { } でネストされた呼び出し 44 dependencies { compile("junit:junit:4.11") }

  45. invoke()を追加 45 class DependencyHandler { fun compile(coordinate: String) { println("Added

    dependency on $coordinate") } operator fun invoke(body:DependencyHandler.() -> Unit) { body() } }
  46. invoke()を追加 46 class DependencyHandler { fun compile(coordinate: String) { println("Added

    dependency on $coordinate") } operator fun invoke(body:DependencyHandler.() -> Unit) { body() } } レシーバー付きラムダ
  47. invoke()を追加 47 class DependencyHandler { fun compile(coordinate: String) { println("Added

    dependency on $coordinate") } operator fun invoke(body:DependencyHandler.() -> Unit) { this.body() } } レシーバー付きラムダ body()はDependencyHandlerをthisとした 拡張関数として呼び出される
  48. gradle Kotlin DSLの例 48 dependencies.invoke( { this.compile("junit:junit:4.11") } ) operator

    fun invoke(body:DependencyHandler.() -> Unit) { this.body() }
  49. gradle Kotlin DSLの例 49 dependencies { compile("junit:junit:4.11") }

  50. 2つの呼び出しを同時に実現 50 dependencies.compile("junit:junit:4.11") dependencies { compile("junit:junit:4.11") }

  51. DSLでよく使われるKotlin機能ランキング 第⼀位

  52. DSLでよく使われるKotlin機能ランキング 第⼀位 レシーバー付きラムダ

  53. 詳細は 53 Kotlin イン・アクションにて

  54. 54 $PQZSJHIU˜4BOTBO *OD"MMSJHIUTSFTFSWFE  4BOTBO͸Ұॹʹ৽͍͠Ձ஋Λ࡞͍ͬͯ͘ ஥ؒΛ͕͍ͯ͞͠·͢ɻ 3VCZ 3VCZPO3BJMT ʢ8FCΞϓϦέʔγϣϯʣ $ɼ"41/&5.7$

    ʢ8FCΞϓϦέʔγϣϯʣ J04"OESPJEΞϓϦ   ݸਓ޲໊͚ࢗ؅ཧΞϓϦʮ&JHIUʯ   ໊ࢗσʔλԽ෼ࢄॲཧγεςϜ   ๏ਓ޲໊͚ࢗ؅ཧαʔϏεʮ4BOTBOʯ   ๏ਓ޲໊͚ࢗ؅ཧαʔϏε ʮ4BOTBOʯ   ݸਓ޲໊͚ࢗ؅ཧΞϓϦʮ&JHIUʯ ΤϯδχΞืूத 4BOTBO࠾༻ ݕࡧ SFDSVJU!TBOTBODPN·Ͱ ͓ؾܰʹ͝࿈བྷ͍ͩ͘͞ɻ ڵຯͷ͋Δํ͸
  55. Enjoy! 55