🥰

Scala 3 の Match Types で再帰型を表現する.

2022/03/11に公開

再帰型、つまり type T = T | S のような型を Scala 3 の Match Types を使って表現する.

再帰型が使えると例えば List("hello",List("world"),"!") にも List("hello","world","!") にも List(List("hello"),"world",List("!")) にもマッチするような型が作れる.

Scala 3 では A | B というふうに Union 型が使えるので型 S を再帰の終端として T = T | S と書けるんちゃうか?と思っていた時期がありました😇

残念ながら type T = T | S と書ければいいがこれは illegal cyclic type reference となってエラーになります. ぴえん.

そこで Scala のユーザーグループに書かれていたチョットしたハックを使う.

build.sbt を書く.

build.sbt
scalaVersion := "3.0.2"
scalacOptions += "-Yexplicit-nulls"

次のような型を定義する.

type Rec[F[_],A] = A match
  case String => String | F[Rec[F,String]]
  case _ => A | F[Rec[F,A]]

こうすると次のコードはすべてコンパイルが通る(どうして....😇)

val list0:Rec[List,String] = List("hello",List("world"),"!")
val list1:Rec[List,String] = List("hello","world","!")
val list2:Rec[List,String] = List(List("hello"),"world",List("!"))
val list3:Rec[List,String] = List(List(List(List(List("hello")))),"world",List("!"))

一方で List(1) などはちゃんとコンパイルエラーになる.

これを応用して Rec型を次のように定義することで Json を表現することもできる.

opaque type JNull = Unit
opaque type JPrimitive = Int | String | Boolean | JNull
type JRec[JArray[_],JObj[_],A] =
  A match
    case JPrimitive => JPrimitive | JArray[JRec[JArray,JObj,JPrimitive]] | JObj[JRec[JArray,JObj,JPrimitive]]
    case _ => A | JArray[JRec[JArray,JObj,A]] | JObj[Rec[JArray,JObj,A]]
type JValue = Rec[Seq,[A] =>> Map[String,A],JPrimitive]

val x0 :JValue = 1 // Ok
val x1 :Jvalue = 1d // => コンパイルエラー
val x1 :JValue = true // Ok
val x2:JValue = Seq(1,"str",false) // Ok
val x3 :JValue = Map("k0"->1,"k1"->Map("k1-1"->Map("k1-1-1"->List(false,1,"2")))) // Ok!
val x4 :JValue = Map("k0"->1,"k1"->Map("k1-1"->Map("k1-1-1"->List(false,1d,"2")))) // => コンパイルエラー

val x1 :JValue = null // => コンパイルエラー

型安全で夢が広がりますね(^ω^)

(正直に言うとこれの使い道をあまり思いつかない...)

以下の記事を参考にした(コドュマーロってなんだ...?🤔)

https://kodumaro.cacilhas.info/2020/11/rec-type.html

Discussion