Circeというライブラリがある。ScalaでJSONまわりの変換とかをやってくれるライブラリだ。
他方、Shapelessというライブラリがある。ScalaでHeterogeneous List、つまりリストなんだけど異なるデータ型を含むリストを扱うためのライブラリで、代数的データ型(ADTs)を操作することができる。 Shapelessがあると、異なるcase class同士を相互変換したり、データ型に依存せずに構造にのみ注目した記述を行えるようになる。
JSONを直和型に写す
で、例えばこういうことをやりたいときがある:
- とあるJSONをデコードしたい
- JSONは、文字列または数値である
素朴に「文字列または数値」をScalaの型で表現すると Either[String, Double]
になるけれど、これがさらに増えると大変:
- JSONは、文字列または数値または文字列の集合である
Either[String, Either[Double, Set[String]]]
- 大変
そこで、shapelessのCoproduct
を使って、「文字列または数値または文字列の集合」を表現して、そこに向けてJSONをデコードすることを考える:
import shapeless._ import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._ /* String or Double or Set of String */ type SDSS = String :+: Double :+: Set[String] :+: CNil val s = """12345.6""" // Double val Right(j) = decode[SDSS](s) // 動かない!!!
circe.shapes
色々と調べてみると、circeがcirce.shapes
というモジュールを提供していることがわかった。
libraryDependencies ++= Seq( "io.circe" %% "circe-core", "io.circe" %% "circe-generic", "io.circe" %% "circe-parser", "io.circe" %% "circe-shapes" // 追加した ).map(_ % circeVersion), libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.9"
そこでcirce.shapes
を依存に加えて以下のように書くと、ShapelessのHList/CoproductとJSONとを相互変換できた:
import shapeless._ import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._ import io.circe.shapes._ type IS = Int :: String :: HNil val myList: IS = 30 :: "car" :: HNil // myList: IS = 30 :: "car" :: HNil val listJson = myList.asJson // listJson: Json = JArray(value = Vector(JNumber(value = JsonLong(value = 30L)), JString(value = "car"))) val ljs = listJson.toString // ljs: String = """[ // 30, // "car" // ]""" val Right(ml2) = decode[IS](ljs) // ml2: IS = 30 :: "car" :: HNil myList == ml2 // res0: Boolean = true type IORS = Int :+: String :+: CNil val s = """123""" // s: String = "123" val s2 = """"death"""" // s2: String = "\"death\"" val Right(iors) = decode[IORS](s) // iors: IORS = Inl(head = 123) val Right(iors2) = decode[IORS](s2) // iors2: IORS = Inr(tail = Inl(head = "death"))
やったー。