Home About Contact
Kotlin , Monad

2024年改訂版) データ変換を Writer Monad 的に処理する

データ変換を Writer Monad 的に処理する その1というポストを一年くらい前に書いたのだが、 今時点で最新の kotlin で作動させようとしたところ、 このコードが作動しなくなっていた。

環境

$ kotlin -version
Kotlin version 2.0.20-release-360 (JRE 17.0.12+7-Ubuntu-1ubuntu222.04)

Writer<T> を修正

動かないコード(以前書いたもの)

import java.util.Optional

class Writer<T> private constructor(val valueOpt: Optional<T>, var text: String) {
    fun <R> bind(f: (T)->Writer<R>): Writer<R> {
        return if( this.valueOpt.isPresent() ) {
            val v: T = this.valueOpt.get()
            val w = f(v)
            w.text = "${this.text} / ${w.text}"
            w
        } else {
            Writer.unit(this.text)
        }
    }

    companion object {
        fun <T> unit(value: T, text: String): Writer<T>{
            return Writer(Optional.of(value), text)
        }

        fun <T> unit(text: String): Writer<T>{
            return Writer(Optional.empty(), text)
        }
    }
}

修正版

修正点は、Optional やめて普通に null を許容すること および 関数名 unit の代わりに `return` を使うことの2点です。

// main.kts

class Writer<T> private constructor(val value: T?, var text: String) {
    fun <R> bind(f: (T)->Writer<R>): Writer<R> {
        return if( value!=null ) {
            val w = f(value)
            w.text = "${this.text} / ${w.text}"
            w
        } else {
            Writer.`return`(this.text)
        }
    }

    companion object {
        fun <T> `return`(value: T, text: String): Writer<T>{
            return Writer(value, text)
        }

        fun <T> `return`(text: String): Writer<T>{
            return Writer(null, text)
        }
    }
}

簡単な例 Writer<Int>

Writer<T>TInt の場合の計算例を考えます。

これが意図通り使えるか試します。

val addOneF: (Int)->Writer<Int> = { value->
    val newValue = value + 1
    Writer.`return`(newValue, "add (${value} +1) = ${newValue}")
}

val subOneF: (Int)->Writer<Int> = { value->
    val newValue = value - 1
    Writer.`return`(newValue, "sub (${value} -1) = ${newValue}")
}

val initValue = 1
val resultWriter =
    Writer.`return`(initValue, "init 1")
    .bind( addOneF )
    .bind( subOneF )

println("result: ${resultWriter.value}")
println("log: ${resultWriter.text}")

実行する。

$ kotlin main.kts
result: 1
log: init 1 / add (1 +1) = 2 / sub (2 -1) = 1

作動しました。 Writer を使う方のコードは変更不要でした。

失敗が起きる場合はどうでしょうか?

絶対失敗する関数 makeErrorF :

val makeErrorF: (Int)->Writer<Int> = { value->
    Writer.`return`("error")
}

この makeErrorF を途中に入れてみます。

val initValue = 1
val resultWriter =
    Writer.`return`(initValue, "init 1")
    .bind( addOneF )
    .bind( makeErrorF )
    .bind( subOneF )

addOneF と subOneF の間に入れてみました。 実行してみます。

$ kotlin main.kts
result: null
log: init 1 / add (1 +1) = 2 / error

結果はエラーになりましたが、意図通り作動しています。 addOneF の次のステップで error になったことも ログに出ています。

実践的な例 Writer<List<Item>>

Writer<T>TList<Item> の場合の計算例を考えます。

計算対象とするデータはこれ。 2023年 と 2024年の商品リストです。

typealias Name = String
enum class Year { Y2023, Y2024 }
data class Item(val name: Name, val price: Int, val year: Year)

val items2023 = listOf(
    Item("Caffe Americano",  500, Year.Y2023),
    Item("Pike Place Roast", 500, Year.Y2023),
    Item("Caffe Misto",      500, Year.Y2023))

val items2024 = listOf(
    Item("Caffe Americano",  600, Year.Y2024),
    Item("Pike Place Roast", 500, Year.Y2024),
    Item("Caffe Misto",      400, Year.Y2024))

この商品リストを使って商品名から List<Item> を見つける関数 findItemsByName :

val findItemsByName: (List<Item>, Name)->List<Item> = { items, name->
    items.filter { it.name==name }
}

これらを使って、 Caffe Americano のアイテムを取り出す処理:

val targetItemName = "Caffe Americano"

val allItems = (items2023 + items2024)
val items = findItemsByName( allItems, targetItemName )
println( items )

実行:

$ kotlin main.kts
[Item(name=Caffe Americano, price=500, year=Y2023), Item(name=Caffe Americano, price=600, year=Y2024)]

targetItemName である Caffe Americano の 2023と2024年のそのアイテムを取り出すことができました。

それでは、 Writer<List<Item>> を使ってみます。

まず 2023,2024年の該当アイテムを計算する関数を定義:

val f2023: (List<Item>)->Writer<List<Item>> = {items->
    val items2023 =
        findItemsByName(allItems, targetItemName).filter { it.year == Year.Y2023 }
    Writer.`return`(items + items2023, "the items of ${targetItemName} in 2023")
}

val f2024: (List<Item>)->Writer<List<Item>> = {items->
    val items2024 =
        findItemsByName(allItems, targetItemName).filter { it.year == Year.Y2024 }
    Writer.`return`(items + items2024, "the items of ${targetItemName} in 2024")
}

この関数を使って計算する処理:

val initValue = listOf<Item>()
val resultWriter = 
    Writer.`return`(initValue, "init")
    .bind(f2023)
    .bind(f2024)

println("result: ${resultWriter.value}")
println("log: ${resultWriter.text}")

実行:

kotlin main.kts
result: [Item(name=Caffe Americano, price=500, year=Y2023), Item(name=Caffe Americano, price=600, year=Y2024)]
log: init / the items of Caffe Americano in 2023 / the items of Caffe Americano in 2024

意図通り 2023, 2024年の Caffe Americano のアイテムを得ることができました。 (そして、その処理過程のログを得ることができました。)

それでは、全部の商品の 2023,2024年のアイテムを得ることにします。

val allNames = allItems.map { it.name }.distinct()
allNames.forEach { targetItemName->

    val f2023: (List<Item>)->Writer<List<Item>> = {items->
        val items2023 =
            findItemsByName(allItems, targetItemName).filter { it.year == Year.Y2023 }
        Writer.`return`(items + items2023, "the items of ${targetItemName} in 2023")
    }
    
    val f2024: (List<Item>)->Writer<List<Item>> = {items->
        val items2024 =
            findItemsByName(allItems, targetItemName).filter { it.year == Year.Y2024 }
        Writer.`return`(items + items2024, "the items of ${targetItemName} in 2024")
    }

    val initValue = listOf<Item>()
    val resultWriter = 
        Writer.`return`(initValue, "init")
        .bind(f2023)
        .bind(f2024)
    
    println("result: ${resultWriter.value}")
    println("log: ${resultWriter.text}")
}

実行:

kotlin main.kts
result: [Item(name=Caffe Americano, price=500, year=Y2023), Item(name=Caffe Americano, price=600, year=Y2024)]
log: init / the items of Caffe Americano in 2023 / the items of Caffe Americano in 2024
result: [Item(name=Pike Place Roast, price=500, year=Y2023), Item(name=Pike Place Roast, price=500, year=Y2024)]
log: init / the items of Pike Place Roast in 2023 / the items of Pike Place Roast in 2024
result: [Item(name=Caffe Misto, price=500, year=Y2023), Item(name=Caffe Misto, price=400, year=Y2024)]
log: init / the items of Caffe Misto in 2023 / the items of Caffe Misto in 2024

できました。

欠損値がある場合

欠損値がある場合を考えます。 ここでは、 Hot Chocolate と Cappuccino が片方の年にしか存在しないデータを例にします。

val items2023 = listOf(
    Item("Caffe Americano",  500, Year.Y2023),
    Item("Pike Place Roast", 500, Year.Y2023),
    Item("Caffe Misto",      500, Year.Y2023),
    Item("Hot Chocolate",    500, Year.Y2023))

val items2024 = listOf(
    Item("Caffe Americano",  600, Year.Y2024),
    Item("Pike Place Roast", 500, Year.Y2024),
    Item("Caffe Misto",      400, Year.Y2024),
    Item("Cappuccino",       500, Year.Y2024))

このデータを処理するコード:

val allNames = allItems.map { it.name }.distinct()
allNames.forEach { targetItemName->

    val f2023: (List<Item>)->Writer<List<Item>> = {items->
        val items2023 =
            findItemsByName(allItems, targetItemName).filter { it.year == Year.Y2023 }

        val log = if( items2023.isNotEmpty() ){
            "OK"
        } else {
            "${targetItemName} is MISSING (in 2023)"
        }

        Writer.`return`(items + items2023, log)
    }
    
    val f2024: (List<Item>)->Writer<List<Item>> = {items->
        val items2024 =
            findItemsByName(allItems, targetItemName).filter { it.year == Year.Y2024 }

        val log = if( items2024.isNotEmpty() ){
            "OK"
        } else {
            "${targetItemName} is MISSING (in 2024)"
        }

        Writer.`return`(items + items2024, log)
    }

    val initValue = listOf<Item>()
    val resultWriter = 
        Writer.`return`(initValue, "init ${targetItemName}")
        .bind(f2023)
        .bind(f2024)
    
    println("result: ${resultWriter.value}")
    println("log: ${resultWriter.text}")
}

今回はログの出し方を工夫しました。 対象アイテムが存在する場合は OK 存在しない場合はその旨をログに書くようにしています。

実行してみます。

$ kotlin main.kts
result: [Item(name=Caffe Americano, price=500, year=Y2023), Item(name=Caffe Americano, price=600, year=Y2024)]
log: init Caffe Americano / OK / OK
result: [Item(name=Pike Place Roast, price=500, year=Y2023), Item(name=Pike Place Roast, price=500, year=Y2024)]
log: init Pike Place Roast / OK / OK
result: [Item(name=Caffe Misto, price=500, year=Y2023), Item(name=Caffe Misto, price=400, year=Y2024)]
log: init Caffe Misto / OK / OK
result: [Item(name=Hot Chocolate, price=500, year=Y2023)]
log: init Hot Chocolate / OK / Hot Chocolate is MISSING (in 2024)
result: [Item(name=Cappuccino, price=500, year=Y2024)]
log: init Cappuccino / Cappuccino is MISSING (in 2023) / OK

この部分:

log: init Hot Chocolate / OK / Hot Chocolate is MISSING (in 2024)

とこの部分:

log: init Cappuccino / Cappuccino is MISSING (in 2023) / OK

のログに欠損値が意図通り報告されています。

まとめ

今回書いたコードを掲載します。

class Writer<T> private constructor(val value: T?, var text: String) {
    fun <R> bind(f: (T)->Writer<R>): Writer<R> {
        return if( value!=null ) {
            val w = f(value)
            w.text = "${this.text} / ${w.text}"
            w
        } else {
            Writer.`return`(this.text)
        }
    }

    companion object {
        fun <T> `return`(value: T, text: String): Writer<T>{
            return Writer(value, text)
        }

        fun <T> `return`(text: String): Writer<T>{
            return Writer(null, text)
        }
    }
}


/*
//
// Example 1: Writer<Int>
//

val addOneF: (Int)->Writer<Int> = { value->
    val newValue = value + 1
    Writer.`return`(newValue, "add (${value} +1) = ${newValue}")
}

val subOneF: (Int)->Writer<Int> = { value->
    val newValue = value - 1
    Writer.`return`(newValue, "sub (${value} -1) = ${newValue}")
}

val makeErrorF: (Int)->Writer<Int> = { value->
    Writer.`return`("error")
}

val initValue = 1
val resultWriter =
    Writer.`return`(initValue, "init 1")
    .bind( addOneF )
    .bind( makeErrorF )
    .bind( subOneF )

println("result: ${resultWriter.value}")
println("log: ${resultWriter.text}")
*/



//
// Example 2: Writer<List<Item>>
//

typealias Name = String
enum class Year { Y2023, Y2024 }
data class Item(val name: Name, val price: Int, val year: Year)


/*
val items2023 = listOf(
    Item("Caffe Americano",  500, Year.Y2023),
    Item("Pike Place Roast", 500, Year.Y2023),
    Item("Caffe Misto",      500, Year.Y2023))

val items2024 = listOf(
    Item("Caffe Americano",  600, Year.Y2024),
    Item("Pike Place Roast", 500, Year.Y2024),
    Item("Caffe Misto",      400, Year.Y2024))
*/

val items2023 = listOf(
    Item("Caffe Americano",  500, Year.Y2023),
    Item("Pike Place Roast", 500, Year.Y2023),
    Item("Caffe Misto",      500, Year.Y2023),
    Item("Hot Chocolate",    500, Year.Y2023))

val items2024 = listOf(
    Item("Caffe Americano",  600, Year.Y2024),
    Item("Pike Place Roast", 500, Year.Y2024),
    Item("Caffe Misto",      400, Year.Y2024),
    Item("Cappuccino",       500, Year.Y2024))


val findItemsByName: (List<Item>, Name)->List<Item> = { items, name->
    items.filter { it.name==name }
}


val allItems = (items2023 + items2024)
val allNames = allItems.map { it.name }.distinct()

allNames.forEach { targetItemName->

    val f2023: (List<Item>)->Writer<List<Item>> = {items->
        val items2023 =
            findItemsByName(allItems, targetItemName).filter { it.year == Year.Y2023 }

        val log = if( items2023.isNotEmpty() ){
            "OK"
        } else {
            "${targetItemName} is MISSING (in 2023)"
        }

        Writer.`return`(items + items2023, log)
    }
    
    val f2024: (List<Item>)->Writer<List<Item>> = {items->
        val items2024 =
            findItemsByName(allItems, targetItemName).filter { it.year == Year.Y2024 }

        val log = if( items2024.isNotEmpty() ){
            "OK"
        } else {
            "${targetItemName} is MISSING (in 2024)"
        }

        Writer.`return`(items + items2024, log)
    }

    val initValue = listOf<Item>()
    val resultWriter = 
        Writer.`return`(initValue, "init ${targetItemName}")
        .bind(f2023)
        .bind(f2024)
    
    println("result: ${resultWriter.value}")
    println("log: ${resultWriter.text}")
}

以上です。