データ変換を Writer Monad 的に処理する その1というポストを一年くらい前に書いたのだが、 今時点で最新の kotlin で作動させようとしたところ、 このコードが作動しなくなっていた。
$ kotlin -version
Kotlin version 2.0.20-release-360 (JRE 17.0.12+7-Ubuntu-1ubuntu222.04)
動かないコード(以前書いたもの)
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<T> の T が 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 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<T> の T が List<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}")
}
以上です。