Kotlin Contracts #m3kt

14c9795d267f5b85abb98ca5e8780646?s=47 Taro Nagasawa
November 07, 2018

Kotlin Contracts #m3kt

どこでもKotlin #6 で発表したスライドです

14c9795d267f5b85abb98ca5e8780646?s=128

Taro Nagasawa

November 07, 2018
Tweet

Transcript

  1. Kotlin Contracts 2018-11-07 長澤 太郎

  2. 長澤太郎 • @ngsw_taro • Ubie株式会社 ソフトウェアエンジニア • エムスリー エンジニアフェロー •

    日本Kotlinユーザグループ代表
  3. 課題: 便利な関数があるのにスマートキャス   トできない(Kotlin 1.3より前) val name: String? = user.name if

    (!name.isNullOrBlank()) { println("Hello, ${name.capitalize()}") }
  4. 課題: 便利な関数があるのにスマートキャス   トできない(Kotlin 1.3より前) val name: String? = user.name if

    (!name.isNullOrBlank()) { println("Hello, ${name.capitalize()}") } stdlibの関数
  5. 課題: 便利な関数があるのにスマートキャス   トできない(Kotlin 1.3より前) val name: String? = user.name if

    (!name.isNullOrBlank()) { println("Hello, ${name.capitalize()}") } Nullableのため 禁止されている呼び出し
  6. 課題: 便利な関数があるのにスマートキャス   トできない(Kotlin 1.3より前) val name: String? = user.name if

    (!name.isNullOrBlank()) { println("Hello, ${name.capitalize()}") } val name: String? = user.name if (name != null && name.isNotBlank()) { println("Hello, ${name.capitalize()}") }
  7. Kotlin 1.3 ではスマートキャストが効く! val name: String? = user.name if (!name.isNullOrBlank())

    { println("Hello, ${name.capitalize()}") } NotNullが保証されてるので 呼び出し可能
  8. Contracts(契約) • 契約による設計/契約プログラミング • 事前条件 ◦ 関数を呼び出すための条件を利用者が満たす ◦ 例)型で不正な値を引数に取らせない ◦

    例)require関数などで不正な値が渡ってきたら例外スロー • 事後条件 ◦ 関数を呼び出したあとの状況を関数が保証する ◦ 例)list#addしたらサイズは1増えることを保証する • 不変条件 ◦ オブジェクトが満たすべき状態を維持する ◦ 例)Nameオブジェクトは1字以上20字以下の英字を必ず持つ
  9. Contracts(契約) • 契約による設計/契約プログラミング • 事前条件 ◦ 関数を呼び出すための条件を利用者が満たす ◦ 例)型で不正な値を引数に取らせない ◦

    例)require関数などで不正な値が渡ってきたら例外スロー • 事後条件 ◦ 関数を呼び出したあとの状況を関数が保証する ◦ 例)list#addしたらサイズは1増えることを保証する • 不変条件 ◦ オブジェクトが満たすべき状態を維持する ◦ 例)Nameオブジェクトは1字以上20字以下の英字を必ず持つ Kotlin Contractsはここ しかも静的
  10. 契約を表明するDSL val name: String? = user.name if (!name.isNullOrBlank()) { println("Hello,

    ${name.capitalize()}") } inline fun CharSequence?.isNullOrBlank(): Boolean { contract { returns(false) implies(this@isNullOrBlank != null) } return this == null || this.isBlank() }
  11. 契約を表明するDSL val name: String? = user.name if (!name.isNullOrBlank()) { println("Hello,

    ${name.capitalize()}") } inline fun CharSequence?.isNullOrBlank(): Boolean { contract { returns(false) implies(this@isNullOrBlank != null) } return this == null || this.isBlank() }
  12. 契約を表明するDSL val name: String? = user.name if (!name.isNullOrBlank()) { println("Hello,

    ${name.capitalize()}") } inline fun CharSequence?.isNullOrBlank(): Boolean { contract { returns(false) implies(this@isNullOrBlank != null) } return this == null || this.isBlank() } falseが返されるとき、この文字列はNotNullであることを意味する
  13. 自分の関数にも契約を @ExperimentalContracts fun Any?.isNotNull(): Boolean { contract { returns(true) implies

    (this@isNotNull != null) } return this != null }
  14. 自分の関数にも契約を @ExperimentalContracts fun Any?.isNotNull(): Boolean { contract { returns(true) implies

    (this@isNotNull != null) } return this != null } DSL自体は不安定 バイナリ互換は維持される
  15. 契約対応している標準関数 kotlin.testも含む • assertTrue • check • require • assertFalse

    • assertNotNull • checkNotNull • requireNotNull 引数がtrueを保証 引数がfalseを保証 引数がNotNullを保証
  16. 課題: val変数の初期化が絶対に成功するはず    なのにできない(Kotlin 1.3より前) val x: Int run {

    x = 12345 } println(x) ラムダの中での初期化を コンパイラはわかってくれ ない
  17. Kotlin 1.3では「1度だけ呼び出される」契約によ り初期化OK val x: Int run { x =

    12345 } println(x)
  18. 契約を表明するDSL val x: Int run { x = 12345 }

    inline fun <R> run(block: () -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() }
  19. 契約を表明するDSL val x: Int run { x = 12345 }

    inline fun <R> run(block: () -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() }
  20. 契約を表明するDSL val x: Int run { x = 12345 }

    inline fun <R> run(block: () -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() } blockが「ちょうど1度だけ」呼び出される
  21. InvocationKind • AT_MOST_ONCE • EXACTLY_ONCE • AT_LEAST_ONCE • UNKNOWN

  22. 契約対応している標準関数 kotlin.testも含む • run • with • apply • also

    • let • takeIf • takeUnless • repeat EXACTLY_ONCE UNKNOWN
  23. 契約DSLまとめ • contract: 関数の先頭に置く必要がある • returns(): 関数の実行が成功したら... • returns(Any?): 引数の値

    true|false|null を返したら... • returnsNotNull(): NotNullを返したら... • callsInPlace: 引数の関数が呼ばれる回数を保証 contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } contract { returns(true) implies (arg != null) }
  24. 総まとめ • スマートキャストや変数初期化において、面倒なことがあった けどKotlin 1.3で解消された • 契約DSLで契約を表明することでコンパイラに意味が伝わる • 自分の関数に契約DSLを使うことができるが不安定 •

    標準ライブラリ関数で契約DSLを使っているものがあるが安 定 • バイナリ互換あり