Sealed クラスの使い途をネットで調べると色の RGB と CMYK の例などが出ていてなるほど と思っていたが、今つくっているアプリで、エディタのコマンドごとに処理を分岐させる部分で Sealed クラスを使うとわかりやすくコードを表現できたので、それを書き残す。
はじめに、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) )
エディタアプリの編集コマンドとして
この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)
}
以上です。