/var/log/jsoizo

メモ帳 技術とか趣味とか

コンストラクタの返却する型を変えたいときのcompanion object + invoke()パターン

こういうクラスがあり、newする時にエラー型を明示して Either<IllegalArgumentException, Email> が返却されるようにしたいとする。

class Email(val address: String) {
    init {
        require(address.contains("@")) { "Invalid email address" }
    }
}

とすると、このように書くことが多いのだが、呼び出しごとに Email.of("foo@bar.com") みたいに書く必要があるのがやや面倒だと感じていた。

class Email private constructor(val address: String) {
    companion object {
        fun of(address: String): Either<IllegalArgumentException, Email> = if (address.contains("@")) {
            Email(address).right()
        } else {
            (IllegalArgumentException("Invalid email address")).left()
        }
    }
}

of をこのようにすると引き続き Email("foo@bar.com") でEmailのインスタンスが作成できるようになる。
また、constructorと違ってcontext receiverやアノテーションもつけられるという点も良い。

operator fun invoke(address: String)): Either<IllegalArgumentException, Email>

ただinvokeに関して、IDE上でジャンプする時に直接Invokeには飛んでくれなくて、invokeが実装されているクラス今回だとcompanion objectに飛んでしまう。これは コンパイラがEmail()をコンパイル時にEmail.companion.invoke()に変換する と振る舞うことをイメージできないとコードの追いづらさが上がるところがあり良し悪しあるなとは感じる。そのあたりを理解して使ったほうが良いかもしれない。

個人レベルでは良いが現場では使いづらいテクニック。

参考

kotlinlang.org

KotestでTable Driven Testする

KotestでTable Driven Testをサポートしているシンタックスがあるのでそれを使えば良い。 JUnitでもやっている例はあるけど、Kotestの方がデータ型の用意が不要だったりするのと、テストの可読性が高そうにみえる(好みの範疇)。

また、Table Drivenにテストを書くことで、テストケースの網羅性が確認しやすいのと、テスト実行のためのボイラープレートを減らせるので、入力パターンが3件以上くらいになってくると良いと思われる。

なおテストの実行上はテーブル内にテストケース名を書かなくても良いが、テスト仕様書として考えると必要であり、個人的なおすすめとしては必ずテーブルの1列目にテストケース名を入れるようにした上で、forAllのブロック内でテストケースの引数だけを _ として無視してあげることである。

fun isMultipleOfThreeOrContainsThree(int: Int): Boolean {
    return int % 3 == 0 || int.toString().contains("3")
}

class TableBasedTestExample : FunSpec({
    test("isMultipleOfThreeOrContainsThree") {
        val table = table(
            headers("case", "given", "expected"),
            row("3の倍数", 12, true),
            row("3を含む", 24357, true),
            row("3の倍数でもなく3も含まない", 44, false)
        )
        forAll(table) { _, given, expected ->
            val result = isMultipleOfThreeOrContainsThree(given)
            assert(result == expected)
        }
    }
})

他の例として、よくあるコンストラクタの中でrequireするような処理の場合、
例外処理になってassertが面倒になるので以下のように一度 Resut<T> でラップしてあげてからassertすると良い。
kotestにResut用のassertion関数が提供されているのでそれでいい感じにできるはず。

data class UserName(val value: String) {
    init {
        val NG_WORDS = listOf(
            "ng_word",
            "ng_word_2"
        )
        require(value.isNotEmpty()) { "UserName is empty" }
        require(!NG_WORDS.any { value.contains(it) }) { "UserName having ng words, value: $value" }
    }
}

class TableBasedTestExample : FunSpec({
    test("UserNameの生成") {
        val table = table<String, String, (Result<UserName>) -> Unit>(
            headers("case", "given", "assertion"),
            row("OKな文字列",
                "Valid Name",
                { it.shouldBeSuccess { it.value.shouldBe("Valid Name") } }),
            row("空文字",
                "",
                { it.shouldBeFailure { it.message.shouldBe("UserName is empty") } }),
            row(
                "NGワード",
                "A ng_word bar",
                { it.shouldBeFailure { it.message.shouldBe("UserName having ng words, value: A ng_word bar") } })
        )
        forAll(table) { _, given, assertionFunc ->
            val result = kotlin.runCatching {
                UserName(given)
            }
            assertionFunc(result)
        }
    }
})

この例をみる限り、大抵の事前条件違反はユニットテストを書いていくことを考えると、インスタンス生成を行う際にバリデーション結果を例外で投げるよりもResultやEither<A,B>のようなデータ型を返したほうがテストの書きやすさが上がるということがなんとなく感じられるはずである。

別解として、 kotest-framework-datatest の記法を使うこともできる。
正直tableを用いいるのと差はないと思うが、ヘッダが用意できる/できないの違いでtableのほうが好きである。

    test("isMultipleOfThreeOrContainsThree withData版") {
        withData(
            Triple("3の倍数", 12, true),
            Triple("3を含む", 24357, true),
            Triple("3の倍数でもなく3も含まない", 44, false)
        ) { (_, given, expected) ->
            val result = isMultipleOfThreeOrContainsThree(given)
            assert(result == expected)
        }
    }

Kotlinで部分適用するためのExtension

部分適用するために以下のようなExtensionを用意しておくと便利。

fun <A, B, C, T> Function3<A, B, C, T>.partial(a: A): (B, C) -> T = {b, c -> invoke(a, b, c)}

例えば以下のような引数を3つ取る関数の場合だとこのようになる。

fun example(foo: Int, bar: String, baz: Long): String = TODO()
val exampleBarBaz = ::example.partial(1)
val result = exampleBarBaz("BarBar", 100L)

関数exampleを ::example としてオブジェクトを参照しているのがポイント。

なお、Function2~4あたりまで何パターンか用意しておくとよいかもしれない。
Function3, 4になると組み合わせが多くなってくるのですべてカバーするべきかは悩ましいところである。

fun <A, B, T> Function2<A, B, T>.partial(a: A): (B) -> T = {b -> invoke(a, b)}
fun <A, B, C, T> Function3<A, B, C, T>.partial(a: A): (B, C) -> T = {b, c -> invoke(a, b, c)}
fun <A, B, C, T> Function3<A, B, C, T>.partial(a: A, b: B): (C) -> T = {c -> invoke(a, b, c)}
fun <A, B, C, D, T> Function4<A, B, C, D, T>.partial(a: A): (B, C, D) -> T = {b, c, d -> invoke(a, b, c, d)}
fun <A, B, C, D, T> Function4<A, B, C, D, T>.partial(a: A, b: B): (C, D) -> T = {c, d -> invoke(a, b, c, d)}
fun <A, B, C, D, T> Function4<A, B, C, D, T>.partial(a: A, b: B, c: C): (D) -> T = {d -> invoke(a, b, c, d)}

Arrow.ktにもそういうExtensionが定義されているのでArrowを使っているプロジェクトならそれも選択肢。
partially1 から partially22 まで存在しており、指定したN番目の引数に対する部分適用がシュッと書けるのが良い点。
そのかわり上記の独自Extensionとは異なり複数の引数に対して部分適用を行うにはちょっと記述量が増えてしまう。

apidocs.arrow-kt.io

fun example(foo: Int, bar: String, baz: Long): String = TODO()
val exampleFooBaz = ::example.partially2("BarBar")
val result = exampleFooBaz(1, 100L)

例えば1番目と3番目の二箇所に対して部分適用したい場合はpartiallyをチェーンさせる。

fun example(foo: Int, bar: String, baz: Long): String = TODO()
fun exampleBar = ::example.partially1(1).partially2(100L)
val result = exampleBar("BarBar")

ちなみにscalaだととてもスッキリ書ける。いいよなぁ。

def example(foo: Int, bar: String, baz: Long): String = ???

val exampleFooBaz = example(_, "BarBar", _)
val result = exampleFooBaz(1, 100L)

(翻訳) Kotlinでの型付きエラー処理

この記事は以下のブログの翻訳記事で、著者の許諾のもと翻訳しています。翻訳過程で機械翻訳を活用しており、原典と翻訳を十分に照らし合わせて内容が正しいことを確認しておりますが、細かいニュアンスなどが気になる場合は原典をご確認ください。また、誤訳などあればtwitterにてご連絡いただけたら修正いたします。

medium.com

目次

  • 目次
  • イントロ
    • 例外(Exceptions)
    • 型付きエラー処理(Typed Error Handling)
  • 比較研究
  • 実験結果
    • 6位: 早期returnなしのsealed classマッチング
    • 5位: Either<L, R> のflatMapチェーン
    • 4位: 例外と再スロー(rethrow)
    • 3位: 早期returnありのsealed classマッチング
    • 2位: Arrowの either { } ビルダー
    • 1位: Arrowの context(Raise<E>) と Context Receivers
  • 結論
  • 出典

以下訳文となります。

続きを読む

複数のバリデーション結果を蓄積したいときのEither<Nel<E>, A>とzipOrAccumurate

よくあるケースとして、入力値に対してバリデーションを行いすべて評価してから結果を返したい場合がある。
たとえばこのようなPerson型があったときに、

class Person private constructor(val name: Name, val age: Age)
  • name は空文字を許さない
  • age は0以上

の要件があったうえで nameが空文字でageが0 のような入力があったら、 1,2それぞれのエラーを返却したい。

こういうときにはArrow.ktのEither型を利用してバリデーションを実装した上で、zipOrAccumulateで蓄積していくのが良い。

class ValidationError(val field: String, val value: String, reason: String)

// コンパニオンオブジェクト以外からnewすることを防ぐ
data class Name private constructor(val value: String) {
    companion object {
        // ファクトリでバリデーションしOKならRight, ダメならLeft
        fun of(value: String): Either<ValidationError, Name> =
            if (value.isNotEmpty()) Name(value).right()
            else ValidationError("Name", value, "input is empty").left()
    }
}
data class Age private constructor(val value: Int) {
    companion object {
        fun of(value: Int): Either<ValidationError, Age> =
            if (value > 0) Age(value).right()
            else ValidationError("Age", value.toString(), "input is less than or equal 0").left()
    }
}

class Person private constructor(val name: Name, val age: Age) {
    companion object {
        fun of(name: String, age: Int): Either<Nel<ValidationError>, Person> = Either.zipOrAccumulate(
            Name.of(name),
            Age.of(age)
        ) { validatedName, validatedAge ->
            Person(validatedName, validatedAge)
        }
    }
}

この例の場合だと結果は以下のようになる。

  1. nameとage両方Right → Personのインスタンスが生成できる
  2. nameはRight, ageはLeft → ageのバリデーションエラーとなる
  3. nameはLeft, ageはRIght → nameのバリデーションエラーとなる
  4. nameとage両方Left → age, name両方のバリデーションエラーとなる

欠点として、 zipOrAccumulateでシュッと積み上げられるのが、 EIther<E, A> もしくは Either<NonEmptyList<E>, A> だけなので、例えば要件として

  • name に禁則文字を含まない

のような要件が追加され、 name に対する要件①と区別したい場合には、Nameのファクトリメソッド戻りの型がこのようになるはずである。

data class Name private constructor(val value: String) {
    companion object {
        fun of(value: String): Either<NonEmptyList<ValidationError>, Name> = TODO()
    }
}

こうするとNameとAgeでファクトリメソッドの型がそれぞれ Either<NonEmptyList<E>, A>Either<E, A> になり zipOrAccumurate で積み上げていくことができなくなるので、Ageのファクトリの型も同じように Either<NonEmptyList<E>, A> としなければならないのが難点である。

とはいえ、任意の値をNonEmptyListに含めるには .nel() すればよいだけなのでそこまで苦労しないはずである。

data class Age private constructor(val value: Int) {
    companion object {
        fun of(value: Int): Either<NonEmptyList<ValidationError>, Age> =
            if (value > 0) Age(value).right()
            else ValidationError("Age", value.toString(), "input is less than or equal 0").nel().left()
}

Arrow.ktのnullable DSLでjOOQがnullable大量に生み出すのを何とかする

過去にjOOQのRecord型から値を取り出すときに、一旦nullableをArrow.ktのOption型に変換してから合成すると良いよ的な書き方をしたんだけど、

jsoizo.hatenablog.com

同じくArrow.ktのnullable DSLでもっとスッキリ書けるとわかったので改めて。

以下のようなコードがあり、 当該フィールドに値が入っていることは確定しているから !! で強制的にnullableを無視している。これでも良いのだけど、コンパイラがめっちゃwarn出してきてただならぬ雰囲気になるんだよね。

val query = create.select().from(TODOS)

val todos = query.fetch().map { record ->
    ToDo(
        id = record.get(TODOS.ID)!!,
        title = record.get(TODOS.TITLE)!!,
        body = record.get(TODOS.BODY)!!,
        created_at = record.get(TODOS.CREATED_AT)!!,
        modified_at. = record.get(TODOS.MODIFIED_AT)!!
    )
}

Arrow.ktのnullable DSLを利用するとこのようにできる。
※ 利用しているArrow.ktのバージョンは 1.2.0-RC

val todos = query.fetch().mapNotNull { record ->
    nullable {
        ToDo(
            id = record.get(TODOS.ID).bind(),
            title = record.get(TODOS.TITLE).bind(),
            body = record.get(TODOS.BODY).bind(),
            created_at = record.get(TODOS.CREATED_AT).bind(),
            modified_at = record.get(TODOS.MODIFIED_AT).bind()
        )
    }
}

万が一どこかの値がnullだった場合に、元のコードとしてはNullPointerExceptionがはっせいしてしまうところであるが、今回のコードだとmapしたあとの ToDo() もnullになり、mapNotNull で消えてくれるので無視することができる。

不用意にOption型に詰め直す必要がないのはとても楽である。

なお、 Raise<T> を利用したパターンでこのように書くこともできる

val todos = query.fetch().mapNotNull { record ->
    nullable {
        ToDo(
            id = ensureNotNull(record.get(TODOS.ID)),
            title = ensureNotNull(record.get(TODOS.TITLE)),
            body = ensureNotNull(record.get(TODOS.BODY)),
            created_at = ensureNotNull(record.get(TODOS.CREATED_AT)),
            modified_at = ensureNotNull(record.get(TODOS.MODIFIED_AT))
        )
    }
}

さらにさらに、このようなjOOQのRecord型の拡張を用意しておくとシュッとしたコードになる。
※ build.gradleでcontext receiverの設定を追加する必要あり

context(NullableRaise)
fun <T> org.jooq.Record.getOrRaise(field: org.jooq.Field<T>) = 
    ensureNotNull(get(field))

val todos = query.fetch().mapNotNull { record ->
    nullable {
        ToDo(
            id = record.getOrRaise(TODOS.ID),
            title = record.getOrRaise(TODOS.TITLE),
            body = record.getOrRaise(TODOS.BODY),
            created_at = record.getOrRaise(TODOS.CREATED_AT),
            modified_at = record.getOrRaise(TODOS.MODIFIED_AT)
        )
    }
}

参考:

arrow-kt.io

Context Receiverを利用した簡易的なDI

Kotlin Confがはじまった!!!

キーノートではKotlin 2.0に追加される言語仕様が発表された。

blog.jetbrains.com

個人的には好きなKEEP段階の機能であるContext Receiverがついに正式なリリースを迎えることになりそうで嬉しい一方で、Kotlin 2.0まで待たないといけないのか。。。という悲しみもあった。

とはいえ、現行の最新版であるKotlin 1.8でもすでに利用可能ではあり大幅に仕様変更をすることもなさそうで既にプロダクションで使えるレベルにあるので、実践的にContext Receiverを使うにはこういう用法が良いだろうということを示すための簡易的なDIのコードと、実際に使ってみてのContext ReceiverでDIすることのメリットを書き記す。

余談だが、この機能を初めて見たとき個人的にはScalaのZIOにおける環境型みたいなものだと思った。
https://zio.dev/overview/summary#zio

Context Receiverの仕様サマリ

Context Receiverの仕様を要約すると以下の通りとなる。

  • 任意の関数宣言の上部に context(Foo, Bar) と記載することで、関数が実行されるコンテキストを明示することができる
  • contextで与えた型のフィールドは関数内部から参照, 呼び出し可能である
  • 関数呼び出しを with(Foo){ } でラップすることで、関数に対して暗黙的にコンテキストを渡すことができる

Context Receiverを利用する上で

現時点では正式な機能ではないため、build.gradleにおけるKotlinコンパイラのオプションに -Xcontext-receivers を追加する必要がある。Kotlin 2.0がリリースされるとこれが不要となる。

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "1.8"
    kotlinOptions.freeCompilerArgs = listOf("-Xcontext-receivers")
}

DIが必要な想定ユースケース

たとえば以下のようなUserドメインクラスがあり、インスタンスの生成ロジック User.create を利用,テストしたいとする

  • ID、名前、作成日時をもつ
  • Userの作成には名前のみが必要でIDはランダムに払い出し作成日時は現在時刻を利用する
  • 名前は少なくとも1文字以上である
class User private constructor(val userId: UUID, val name: String, val createdAt: OffsetDateTime) {

    companion object {
        fun create(name: String): User = run {
            if (name.isEmpty()) throw IllegalArgumentException("User.name is empty!!")
            User(UUID.randomUUID(), name, OffsetDateTime.now())
        }
    }

}

ここで、

IDはランダムに払い出し作成日時は現在時刻を利用する

という仕様があるが、この状態だと id, createdAt フィールドは値の生成時に副作用を伴い実行時点の乱数や時計に依存するためにテスト時のassertが難しい(※)。ということでClockやUUIDをDIすることで User.create から副作用を切り離して差し替えられるようにし、モックによるによるテストが出来るようにしたくなる。

※ あくまでサンプルとして単純化しただけで、特にロジックのない id, createdAt フィールドのassertは要らんだろ的なことは承知している

これを単純に解こうとすると、User.create を以下のように直すことになるが、 、、

fun User.create(name: String, generateUUID: () -> UUID, clock: Clock): User = run {
    if (name.isEmpty()) throw IllegalArgumentException("User.name is empty!!")
    User(generateUUID(), name, OffsetDateTime.now(clock))
}

fun main(args: Array<String>) {
    val generateUUID = UUID::randomUUID
    val clock = Clock.systemDefaultZone()
    val user = User.create("John", generateUUID, clock)
}

これだとたしかに副作用を伴うUUIDとClockをを関数に対して渡せるようになったため、テストしやすさの点では良くなった。

val dummyDateTime = OffsetDateTime.parse("2023-01-30T12:34:56Z")
val fixedClock = Clock.fixed(dummyDateTime.toInstant(), ZoneId.of("UTC"))
val dummyUUID = UUID.fromString("5ac47e1a-19a9-408c-96d1-6741e8701433")

@Test
fun userCreateTest() {
    val user = User.create("John", { dummyUUID }, fixedClock)
    assert(user.userId == dummyUUID)
    assert(user.name == "John")
    assert(user.createdAt == dummyDateTime)
}

一方で、引数を増やしたことによって

Userの作成には名前のみが必要

という要件がメソッドシグネチャからわかりづらくなり、可読性を落とすことになってしまっている。

Context Receiverを利用してDIしてみる

まずはUUID, Clockそれぞれのコンテキスト型を定義して User.create にレシーバを追加する。

interface UUIDContext {
    fun generateUUID(): UUID
}

interface ClockContext {
    val clock: Clock
}

context(UUIDContext, ClockContext)
fun User.create(name: String): User = run {
    if (name.isEmpty()) throw IllegalArgumentException("User.name is empty!!")
    User(generateUUID(), name, OffsetDateTime.now(clock))
}

呼び出し側ではそれぞれのコンテキスト型を具象化したオブジェクトを with により関数呼び出しのコンテキストとして宣言。 User.create に対して暗黙的に ApplicationContext がそれぞれのコンテキスト型の値として渡ることとなる。

object ApplicationContext : UUIDContext, ClockContext {
    override fun generateUUID(): UUID = UUID.randomUUID()
    override val clock: Clock = Clock.systemDefaultZone()
}

fun main(args: Array<String>) {
    with(ApplicationContext) {
        val user = User.create("John")
    }
}

テストのときは同じようにモックを使ったContextを作れば良い。

@Test
fun userCreateTest() {
    val user = with(testContext) {
        User.create("John")
    }
    assert(user.userId == dummyUUID)
    assert(user.name == "John")
    assert(user.createdAt == dummyDateTime)
}

これにより User.createシグネチャから仕様がわかるし、依存がcontext receiverに明示されているので、Userは見通しの良いコードとなった。

Context Receiverを利用したDIのメリット

個人的にはDIのツールとしてContext Receiverを利用することの最大のメリットは、 依存関係と引数を分離することができる 点に尽きると考えている。

単純なDIとしての利便性だけでいうとおそらくGuiceやSpring、AndroidならHiltのようなDIコンテナのほうが高いと思われるが、これらはあくまで引数として宣言したものに対して動的にオブジェクトを注入するので、 ドメインオブジェクトのように メソッドのシグネチャでドメインの要件を表現したい 場合の見通しを良くしようと思うとこのContext Receiverを用いたDIが活きてくるのではなかろうか。