Home About Contact
Kotlin , Functional Programming , Monad

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

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

前回のエントリーのコードを引き継いで、 もし処理対象のデータに重複があったらどのように対処するか考えてみます。

環境と Writer

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

重複のあるデータを用意

次のように Caffe AmericanoPike Place Roast が(間違って)重複しているデータがあったとします。

typealias XlsxRow = Pair<String,Int>

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

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

件数がこれだけ少なくて連続して間違えることは実際にはないのですが、あくまで例ということでご了承ください。

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

存在する商品名のリストを計算。

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

実行して確認します。

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

これは問題ありません。

findItems 関数を使って、重複している Caffe AmericanoList<Item> を取得してみます。

val nameCaffeAmericano = "Caffe Americano"
val items2023 = findItems(itemList, nameCaffeAmericano, Year.Y2023)
println( items2023 )

実行します。

$ kotlinc -script main.kts
[Item(name=Caffe Americano, price=500, year=Y2023), Item(name=Caffe Americano, price=550, year=Y2023)]

わざと例として重複させているので当然ですが、2件取得できました。 価格が同じなら一つにまとめてしまえばよいのですが、価格が 500 と 550 円になっているので、こちらではどちらが正しいのかわかりません。

同様に Pike Place RoastfindItems してみます。

val namePikePlaceRoast = "Pike Place Roast"
val items2024 = findItems(itemList, namePikePlaceRoast, Year.Y2024)
println( items2024 )

実行。

$ kotlinc -script main.kts
[Item(name=Pike Place Roast, price=500, year=Y2024), Item(name=Pike Place Roast, price=550, year=Y2024)]

2つ出現しました。

複数出現に対応するように returnWriter 関数を修正します。 以前はシグニチャを (List<Item>)->Writer<Optional<Item>> にしていたところを (List<Item>)->Writer<Optional<List<Item>>> に変更します。

returnWriter 全体では次のようになります。

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

これを使って全部の商品名に対して 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 )
}

実行すると次のような結果が得られます。

$ kotlinc -script main.kts
--- Caffe Americano ---
Optional[(Optional[[Item(name=Caffe Americano, price=500, year=Y2023), Item(name=Caffe Americano, price=550, 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), Item(name=Pike Place Roast, price=550, 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

複数見つかった場合はその商品アイテムが両方とも値として列挙されるようになりました。 しかし、ログを見ると OK になっています。

本来一つの商品名に対してアイテムは一つしかないことが想定されているので、その場合は 問題あり、としてログに WARNING を出力するように returnWriter 関数を 変更しましょう。

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

これで実行してみます。

$ kotlinc -script main.kts
--- Caffe Americano ---
Optional[(Optional[[Item(name=Caffe Americano, price=500, year=Y2023), Item(name=Caffe Americano, price=550, year=Y2023)]], Optional[[Item(name=Caffe Americano, price=600, year=Y2024)]])]
WARNING / 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), Item(name=Pike Place Roast, price=550, year=Y2024)]])]
OK / WARNING / 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

商品アイテムが重複している部分でログに WARNING を出すことができました。

ここでは警告として扱う問題が商品名の重複しかないので、これはこれで構いません。 しかし、もし他にも警告したい問題がある場合は WARNING とだけ出てもどの問題か判別できません。 そのような場合に備えて、次のように returnWriter 関数を修正して、問題に関する情報をログとして出力しましょう。

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

実行します。

$ kotlinc -script main.kts
--- Caffe Americano ---
Optional[(Optional[[Item(name=Caffe Americano, price=500, year=Y2023), Item(name=Caffe Americano, price=550, year=Y2023)]], Optional[[Item(name=Caffe Americano, price=600, year=Y2024)]])]
WARNING (2 items found: Caffe Americano)  / 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), Item(name=Pike Place Roast, price=550, year=Y2024)]])]
OK / WARNING (2 items found: Pike Place Roast)  / OK
--- Caffe Misto ---
Optional[(Optional[[Item(name=Caffe Misto, price=500, year=Y2023)]], Optional[[Item(name=Caffe Misto, price=400, year=Y2024)]])]

WARNING (2 items found: Caffe Americano) のように、 どの商品名がどれだけ重複しているかログで表現できました。 必要であれば価格なども併記したログを出すこともできます。

まとめ

Writer を使って、 商品名が誤って重複しているデータへの対処(例)を説明しました。

予め支給データにどんなエラーが潜んでいるか完全にわかっていれば、対処はそれほど難しくないのですが、 現実には、処理を進めるなかで次々発覚していくのが普通でしょう。 その時点で都度対処コードを書いていくことになります。

Writer Monad 的な発想でコードを書いていると、 ここで示したように問題点に気づいた時点で段階的にエラーや例外に対処できるだけでなく、 その対処コードも局所的な変更におさえることができます。

最後に今回のコードを全体を掲載します。

main.kts

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("Caffe Americano", 550),
    Pair("Pike Place Roast", 500),
    Pair("Caffe Misto", 500))

val xlsxRowList2024 = listOf<XlsxRow>(
    Pair("Caffe Americano", 600),
    Pair("Pike Place Roast", 500),
    Pair("Pike Place Roast", 550),
    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)

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

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