Home About Contact
Kotlin , Functional Programming , Monad

データ変換を Writer Monad 的に処理する その3

このポストはすでに古い。 こちら 改訂版 を参照ください。

前回のエントリーの コードを見直しある意味もう少しシンプルに実装してみます。

環境と Writer

実行環境や Writer の実装は 以前のエントリーと同じなのでそちらを見てください。

簡単なデータで試す

前回のエントリーと同じデータから 出発します。

typealias XlsxRow = Pair<String,Int>

val xlsxRowList2023 = listOf<XlsxRow>(
    Pair("Caffe Americano", 500),
    Pair("Pike Place Roast", 500),
    Pair("Caffe Misto", 500))

val xlsxRowList2024 = listOf<XlsxRow>(
    Pair("Caffe Americano", 600),
    Pair("Pike Place Roast", 500),
    Pair("Caffe Misto", 400))

ヘルパー関数などの定義をします。(前回と同じです。)

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

val toItemList: (List<XlsxRow>, List<XlsxRow>)-> List<Item> = { xlsxRowList2023, xlsxRowList2024->
    val itemList2023 = xlsxRowList2023.map { Item(it.first, it.second, Year.Y2023) }
    val itemList2024 = xlsxRowList2024.map { Item(it.first, it.second, Year.Y2024) }
    itemList2023 + itemList2024
}

val toNameList: (List<Item>)-> List<Name> = { itemList->
    itemList.map { it.name }.distinct()
}

val findItems: (List<Item>, Name, Year)-> List<Item> = { itemList, name, year->
    itemList.filter { it.name == name && it.year == year }
}

これらを使って商品名の一覧を取得します。

val itemList = toItemList(xlsxRowList2023, xlsxRowList2024)
val nameList = toNameList(itemList)
println(nameList)

実行します。

$ kotlinc -script main.kts
[Caffe Americano, Pike Place Roast, Caffe Misto]

すべての商品名が取得できました。

次に Writer を使って、商品名ごとに 2023年と2024年 の商品アイテムを結びつけてペアで出力します。

nameList.forEach { name->
    val items2023 = findItems(itemList, name, Year.Y2023)
    val items2024 = findItems(itemList, name, Year.Y2024)

    val resultWriter =
        returnWriter(items2023).bind { item2023->
            returnWriter(items2024).bind { item2024->
                Writer.unit(Pair(item2023, item2024), "OK")
            }
        }

    println( "--- ${name} ---" )
    println( resultWriter.valueOpt )
    println( resultWriter.text )
}

前回とは異なり、bind を入れ子にすることでコードをシンプルにしました。 ここで使用している returnWriter 関数の実装はこれです。

val returnWriter: (List<Item>)->Writer<Item> = { items->
    if( items.size>0 ){
        Writer.unit(items.first(), "OK")
    } else {
        Writer.unit("NG")
    }
}

findItems 関数を使って商品名と年から Item を探しますが、結果は List<Item> になります。 このとき該当アイテムが見つかればログに OK と書き出した上で、該当アイテムを Writer に包んで返します。もし見つからない場合は NG として扱いたいので Writer.unit("NG") を返します。

それでは実行してみます。

$ kotlinc -script main.kts
--- Caffe Americano ---
Optional[(Item(name=Caffe Americano, price=500, year=Y2023), Item(name=Caffe Americano, price=600, year=Y2024))]
OK / OK / OK
--- Pike Place Roast ---
Optional[(Item(name=Pike Place Roast, price=500, year=Y2023), Item(name=Pike Place Roast, price=500, year=Y2024))]
OK / OK / OK
--- Caffe Misto ---
Optional[(Item(name=Caffe Misto, price=500, year=Y2023), Item(name=Caffe Misto, price=400, year=Y2024))]
OK / OK / OK

うまく商品名で 2023年と2024年のペアを取得できています。

欠損アイテムのあるデータで試す

欠損のあるデータに差し替えて実行してみます。

val xlsxRowList2023 = listOf<XlsxRow>(
    Pair("Caffe Americano", 500),
    Pair("Pike Place Roast", 500),
    Pair("Caffe Misto", 500),
    Pair("Hot Chocolate", 500))

val xlsxRowList2024 = listOf<XlsxRow>(
    Pair("Caffe Americano", 600),
    Pair("Pike Place Roast", 500),
    Pair("Caffe Misto", 400),
    Pair("Cappuccino", 500))

これも前回使用したものと同じですが、 Hot ChocolateCappuccino が片方の年にしか存在しないアイテムになっています。

実行してみます。

--- Caffe Americano ---
Optional[(Item(name=Caffe Americano, price=500, year=Y2023), Item(name=Caffe Americano, price=600, year=Y2024))]
OK / OK / OK
--- Pike Place Roast ---
Optional[(Item(name=Pike Place Roast, price=500, year=Y2023), Item(name=Pike Place Roast, price=500, year=Y2024))]
OK / OK / OK
--- Caffe Misto ---
Optional[(Item(name=Caffe Misto, price=500, year=Y2023), Item(name=Caffe Misto, price=400, year=Y2024))]
OK / OK / OK
--- Hot Chocolate ---
Optional.empty
OK / NG
--- Cappuccino ---
Optional.empty
NG

プログラムは通りましたが、商品 Hot ChocolateCappuccino の値は Optional.empty になっていて、ログの最後は NG で終わっています。 片方の年に存在している場合でもアイテム情報を得たいので、これでは不都合です。

ということで、 前回同様にアイテムが見つからなかった場合に対応するため、Item そのものを使うのではなく Optional<Item> を使うように変更してみます。 そのためには returnWriter 関数を次のように書きかえます。

val returnWriter: (List<Item>)->Writer<Optional<Item>> = { items->
    if( items.size>0 ){
        Writer.unit(Optional.of(items.first()), "OK")
    } else {
        Writer.unit(Optional.empty(), "NG")
    }
}

まず、シグニチャを (List<Item>)->Writer<Optional<Item>> に変更。 それから該当アイテムが存在する場合はそのアイテムを Optional に包んだ上でそれを Writer に包んで返します。 該当アイテムが存在しない場合は、エラー扱いの Writer.unit("ログ") を呼ぶ代わりに Optional.empty() を渡してそれを Writer に包んで返します。

変更はこれだけです。 それでは、実行してみます。

$ kotlinc -script main.kts
--- Caffe Americano ---
Optional[(Optional[Item(name=Caffe Americano, price=500, year=Y2023)], Optional[Item(name=Caffe Americano, price=600, year=Y2024)])]
OK / OK / OK
--- Pike Place Roast ---
Optional[(Optional[Item(name=Pike Place Roast, price=500, year=Y2023)], Optional[Item(name=Pike Place Roast, price=500, year=Y2024)])]
OK / OK / OK
--- Caffe Misto ---
Optional[(Optional[Item(name=Caffe Misto, price=500, year=Y2023)], Optional[Item(name=Caffe Misto, price=400, year=Y2024)])]
OK / OK / OK
--- Hot Chocolate ---
Optional[(Optional[Item(name=Hot Chocolate, price=500, year=Y2023)], Optional.empty)]
OK / NG / OK
--- Cappuccino ---
Optional[(Optional.empty, Optional[Item(name=Cappuccino, price=500, year=Y2024)])]
NG / OK / OK

欠損のある商品 Hot Chocolate , Cappuccino についても Item 情報を出力できるようになりました。

まとめ

完成したコード全体を掲載します。

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)
        }
    }
}


typealias XlsxRow = Pair<String,Int>

/*
val xlsxRowList2023 = listOf<XlsxRow>(
    Pair("Caffe Americano", 500),
    Pair("Pike Place Roast", 500),
    Pair("Caffe Misto", 500))

val xlsxRowList2024 = listOf<XlsxRow>(
    Pair("Caffe Americano", 600),
    Pair("Pike Place Roast", 500),
    Pair("Caffe Misto", 400))
*/

val xlsxRowList2023 = listOf<XlsxRow>(
    Pair("Caffe Americano", 500),
    Pair("Pike Place Roast", 500),
    Pair("Caffe Misto", 500),
    Pair("Hot Chocolate", 500))

val xlsxRowList2024 = listOf<XlsxRow>(
    Pair("Caffe Americano", 600),
    Pair("Pike Place Roast", 500),
    Pair("Caffe Misto", 400),
    Pair("Cappuccino", 500))


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

val toItemList: (List<XlsxRow>, List<XlsxRow>)-> List<Item> = { xlsxRowList2023, xlsxRowList2024->
    val itemList2023 = xlsxRowList2023.map { Item(it.first, it.second, Year.Y2023) }
    val itemList2024 = xlsxRowList2024.map { Item(it.first, it.second, Year.Y2024) }
    itemList2023 + itemList2024
}

val toNameList: (List<Item>)-> List<Name> = { itemList->
    itemList.map { it.name }.distinct()
}

val findItems: (List<Item>, Name, Year)-> List<Item> = { itemList, name, year->
    itemList.filter { it.name == name && it.year == year }
}

val returnWriter: (List<Item>)->Writer<Optional<Item>> = { items->
    if( items.size>0 ){
        Writer.unit(Optional.of(items.first()), "OK")
    } else {
        Writer.unit(Optional.empty(), "NG")
    }
}

val itemList = toItemList(xlsxRowList2023, xlsxRowList2024)
val nameList = toNameList(itemList)
println(nameList)

nameList.forEach { name->
    val items2023 = findItems(itemList, name, Year.Y2023)
    val items2024 = findItems(itemList, name, Year.Y2024)

    val resultWriter =
        returnWriter(items2023).bind { item2023->
            returnWriter(items2024).bind { item2024->
                Writer.unit(Pair(item2023, item2024), "OK")
            }
        }

    println( "--- ${name} ---")
    println( resultWriter.valueOpt )
    println( resultWriter.text )
}

bind で処理をつなげていく部分を入れ子にすることで、コードがシンプルかつ直感的で読みやすくなりました。

ここまでコードをシンプルにできたので、いよいよ、次回は、商品名が重複していたり、微妙に異なる商品名を持つアイテムを結びつけるコードを入れていきたい・・・と思います。

以上です。