Home About Contact
Kotlin

Kotlin Sealed クラスの便利さ

Sealed クラスの使い途をネットで調べると色の RGB と CMYK の例などが出ていてなるほど と思っていたが、今つくっているアプリで、エディタのコマンドごとに処理を分岐させる部分で Sealed クラスを使うとわかりやすくコードを表現できたので、それを書き残す。

RGB と CMYK が混在したリストを RGB に統一

はじめに、RGB と CMYK を Sealed class で表現する例を考えてみる。

Color という名前の Sealed class をつくり、そこに RGB, CMYK を定義。

sealed class Color {
    data class RGB(val r: Int, val g: Int, val b: Int): Color()
    data class CMYK(val c: Int, val m: Int, val y: Int, val k: Int): Color()
}

CMYK を RGB に変換する関数を定義(ダミーです)。

// ダミーの実装です。(どんな色も RGBの赤に変換)
val fromCMYK2RGB: (Color.CMYK)->Color.RGB = { cmyk->
    Color.RGB(255,0,0)
}

そして、RGB と CMYK が混在したリスト mixColorList があるとする。

val blue  = Color.RGB(0,255,0)
val white = Color.RGB(255,255,255)
val red   = Color.CMYK(0, 90, 100, 0)

val mixColorList = listOf(blue, white, red)

これを RGB カラーに統一したい、という場合を考える。 つまり、こんなタイプをもつ関数がほしい。

typealias ColorConverterToRGB = (List<Color>)->List<Color.RGB>

別に typealias するほどの話じゃないけれども。

ColorConverterToRGB タイプを持つ、 toRGBColorList の実装。

val toRGBColorList: ColorConverterToRGB = { list->
    list.map { color->
      when(color){
          is Color.RGB -> {
              color
          }
          is Color.CMYK -> {
              fromCMYK2RGB(color)
          }
      }
    }
}

あとは、 mixColorList をこの関数に適用するだけです。

println( toRGBColorList(mixColorList) )

実行。

$ kotlin -version
Kotlin version 1.8.10-release-430 (JRE 17.0.9+9-Ubuntu-122.04)
$ kotlin main.kts
[RGB(r=0, g=255, b=0), RGB(r=255, g=255, b=255), RGB(r=255, g=0, b=0)]

RGBに統一できました。

main.kts 全体を掲載。

sealed class Color {
    data class RGB(val r: Int, val g: Int, val b: Int): Color()
    data class CMYK(val c: Int, val m: Int, val y: Int, val k: Int): Color()
}

// ダミーの実装です。(どんな色も RGBの赤に変換)
val fromCMYK2RGB: (Color.CMYK)->Color.RGB = { cmyk->
    Color.RGB(255,0,0)
}

typealias ColorConverterToRGB = (List<Color>)->List<Color.RGB>

val toRGBColorList: ColorConverterToRGB = { list->
    list.map { color->
      when(color){
          is Color.RGB -> {
              color
          }
          is Color.CMYK -> {
              fromCMYK2RGB(color)
          }
      }
    }
}

val blue  = Color.RGB(0,255,0)
val white = Color.RGB(255,255,255)
val red   = Color.CMYK(0, 90, 100, 0)

val mixColorList = listOf(blue, white, red)

println( toRGBColorList(mixColorList) )

コマンドを Sealed クラスで表現する

エディタアプリの編集コマンドとして

この3つのコマンドを表現したいとする。 そして、コマンドが起きた結果をデータベースに保存する処理があるのだが、 ほとんどの処理は共通だが、一部だけがコマンドごとに異なる。 そういう関数 saveCmdToDb を考える。

事前準備として ストロークを 次のように表現します。

typealias StrokeID = String
typealias Points = List<Pair<Float,Float>>
data class Stroke(val id: StrokeID, val points: Points)

これを使って、コマンドを表現します。

sealed class Cmd {
    data class AddOneStroke(val stroke: Stroke): Cmd()
    data class AddStrokes(val strokeList: List<Stroke>): Cmd()
    data class DelStrokes(val strokeIdList: List<StrokeID>): Cmd()
}

削除するときは、 StrokeID を指せばよい、という実装です。

それでは、このコマンドをデータベースに保存する saveCmdToDb を考えます。

タイプ SaveCmd を考えると次のようになります。

typealias SaveCmd =(Cmd)->Unit

コマンドがきて・・・それだけです。関数内部で(副作用が発生して)データベースとかファイルに Cmd の内容が保存されることが期待されている、を表現しています。

このタイプを実装した saveCmdToDb 関数を書きます。

val saveCmdToDb: SaveCmd = { cmd->
    val json = when(cmd) {
        is Cmd.AddOneStroke-> {
            fromStrokeToJson(cmd.stroke)
        }
        is Cmd.AddStrokes-> {
            fromStrokesToJson(cmd.strokeList)
        }
        is Cmd.DelStrokes-> {
            fromStrokeIdsToJson(cmd.strokeIdList)
        }
    }
    println(json)
}

コマンドをその種類に応じて json 文字列に変換して println しています。

本来はここでデータベースにこの json 文字列を保存することを想定。

補助関数 fromStrokeToJson, fromStrokesToJson, fromStrokeIdsToJson は次の通り。

val fromStrokeToJson: (Stroke)->String = { stroke->
    val pts = stroke.points.map { listOf(it.first, it.second) }.flatten().joinToString(",")
    "{\"id\": \"${stroke.id}\",\"pts\":\"$pts\"}"
}

val fromStrokesToJson: (List<Stroke>)->String = { strokeList->
    val strokesJson = strokeList.map { fromStrokeToJson(it) }.joinToString(",")
    "{\"strokes\": [$strokesJson]}"
}

val fromStrokeIdsToJson: (List<StrokeID>)->String = { strokeIdList->
    val strokeIdsJson = strokeIdList.map { "\"$it\"" } .joinToString(",")
    "{\"strokeIds\": [$strokeIdsJson]}"
}

あとは実際に Stroke データと Cmd を生成して、意図通り作動するか確認します。

val stroke0 = Stroke("1", listOf(Pair(0f,0f), Pair(10f,10f)))
val stroke1 = Stroke("2", listOf(Pair(10f,10f), Pair(20f,20f)))
val stroke2 = Stroke("3", listOf(Pair(20f,20f), Pair(30f,30f)))

val cmd0 = Cmd.AddOneStroke(stroke0)
val cmd1 = Cmd.AddStrokes(listOf(stroke1,stroke2))
val cmd2 = Cmd.DelStrokes(listOf(stroke0,stroke1,stroke2).map {it.id})

listOf(cmd0, cmd1, cmd2).forEach { cmd->
    saveCmdToDb(cmd)
}

実行してみます。

$ kotlin main.kts
{"id": "1","pts":"0.0,0.0,10.0,10.0"}
{"strokes": [{"id": "2","pts":"10.0,10.0,20.0,20.0"},{"id": "3","pts":"20.0,20.0,30.0,30.0"}]}
{"strokeIds": ["1","2","3"]}

うまくいきました。

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

main.kts

typealias StrokeID = String
typealias Points = List<Pair<Float,Float>>
data class Stroke(val id: StrokeID, val points: Points)

sealed class Cmd {
    data class AddOneStroke(val stroke: Stroke): Cmd()
    data class AddStrokes(val strokeList: List<Stroke>): Cmd()
    data class DelStrokes(val strokeIdList: List<StrokeID>): Cmd()
}


val fromStrokeToJson: (Stroke)->String = { stroke->
    val pts = stroke.points.map { listOf(it.first, it.second) }.flatten().joinToString(",")
    "{\"id\": \"${stroke.id}\",\"pts\":\"$pts\"}"
}

val fromStrokesToJson: (List<Stroke>)->String = { strokeList->
    val strokesJson = strokeList.map { fromStrokeToJson(it) }.joinToString(",")
    "{\"strokes\": [$strokesJson]}"
}

val fromStrokeIdsToJson: (List<StrokeID>)->String = { strokeIdList->
    val strokeIdsJson = strokeIdList.map { "\"$it\"" } .joinToString(",")
    "{\"strokeIds\": [$strokeIdsJson]}"
}


typealias SaveCmd = (Cmd)->Unit

val saveCmdToDb: SaveCmd = { cmd->
    val json = when(cmd) {
        is Cmd.AddOneStroke-> {
            fromStrokeToJson(cmd.stroke)
        }
        is Cmd.AddStrokes-> {
            fromStrokesToJson(cmd.strokeList)
        }
        is Cmd.DelStrokes-> {
            fromStrokeIdsToJson(cmd.strokeIdList)
        }
    }
    println(json)
}

val stroke0 = Stroke("1", listOf(Pair(0f,0f), Pair(10f,10f)))
val stroke1 = Stroke("2", listOf(Pair(10f,10f), Pair(20f,20f)))
val stroke2 = Stroke("3", listOf(Pair(20f,20f), Pair(30f,30f)))

val cmd0 = Cmd.AddOneStroke(stroke0)
val cmd1 = Cmd.AddStrokes(listOf(stroke1,stroke2))
val cmd2 = Cmd.DelStrokes(listOf(stroke0,stroke1,stroke2).map {it.id})

listOf(cmd0, cmd1, cmd2).forEach { cmd->
    saveCmdToDb(cmd)
}

以上です。