もう9年以上前に以下のようなものを書きましたが、それに関連する話。
簡単に状況というか前提を整理すると
- Scala 2.12以前はSIP-23が存在しなかったので、ある意味上記のようなshapless 2の黒魔術が発明された
- Scala 2.13からSIP-23が入った。つまり
val x: 2 = 2
みたいに、型の部分にIntやStringのリテラルが書けるようになった - Scala 3でも、もちろんSIP-23は引き続き使える
- Scala 3ではshapeless 2自体がビルドされていないので、もちろん以下のような記法は無理
Witness.`123`.T
よって
- Scala 2.13以前だけcross buildするならshapeless使えば良い
- Scala 2.13と3だけならSIP-23で良い
- Scala 2.12, 2.13, 3と、3つの世代でcross buildしたいときに困る
となるはずです。まぁ今どきScala 2.12を切り捨ててScala 2.13以降にするのが健全でしょうけれど、色々あってそういうのが無理な場合もあるでしょう。
一例を挙げると、refinedというライブラリが、これを書いてる2024年1月の時点では、その3世代をサポートしているので、 ほとんど同じコードなのに、この問題のために、ファイル分けて書かれています。以下、つらい例
- https://github.com/fthomas/refined/blob/71cfd8a7c9aa1c07d974ad0ca254c5e2dc7b871b/modules/core/shared/src/main/scala-3.0-/eu/timepit/refined/types/net.scala
- https://github.com/fthomas/refined/blob/71cfd8a7c9aa1c07d974ad0ca254c5e2dc7b871b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/types/net.scala
さて、現時点で思いついた解決策(= 出来るだけ重複を避ける)を書いていきます。
Scala 3でmatch typeとdynamicでshapeless 2と同じ記法で書けるようにする
以下、とりあえずIntの例です。 同じ原理でStringやその他も可能なはずです。 objectに無理やりtypeを追加しているのが、少し無理やり感があって、今後のScala 3で動き続けるのか?が不安、というのはあります。 もっと健全な方法ないんでしょうか・・・? あと、ひとまずmatch typeで書けたので書きましたが、コンパイル速度その他の細かい点を考慮すると、あえてmacro使って書いた方がいい可能性もあるけれど、詳細は未検証。 Doubleのリテラルのparser真面目に頑張ったら超辛そう、なども…
import scala.compiletime.ops.string.Length import scala.compiletime.ops.string.CharAt import scala.compiletime.ops.string.Substring import scala.compiletime.ops.int import scala.language.dynamics object Witness extends Dynamic { type StringToInt[Input <: String] = Loop[Input, 0] type CharToInt[C <: Char] <: Int = C match { case '0' => 0 case '1' => 1 case '2' => 2 case '3' => 3 case '4' => 4 case '5' => 5 case '6' => 6 case '7' => 7 case '8' => 8 case '9' => 9 } type Loop[Input <: String, Acc <: Int] <: Int = Length[Input] match { case 0 => Acc case _ => Loop[ Substring[Input, 1, Length[Input]], int.+[ int.*[10, Acc], CharToInt[ CharAt[Input, 0] ] ] ] } def selectDynamic( selector: String ): WitnessValue.type { type T = StringToInt[selector.type] } = WitnessValue.asInstanceOf[ WitnessValue.type { type T = StringToInt[selector.type] } ] } object WitnessValue
scalafixでbuild時に書き換えてコード生成
project/plugins.sbt
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % "0.11.1"
project/なんとか.scala
import scala.meta.Lit import scala.meta.Term import scala.meta.Tree import scala.meta.Type import scala.meta.XtensionQuasiquoteImporter import scala.meta.inputs.Input import scalafix.Patch import scalafix.internal.config.ScalaVersion import scalafix.v1.SyntacticDocument object RefinedTypeCompat { private def createPatch(tree: Tree): Patch = Seq( Patch.addGlobalImport( importer"eu.timepit.refined.W" ), tree.collect { case Type.ArgClause(args) => args.collect { case n: Lit => Patch.replaceTree( n, Type .Select( Term.Select(Term.Name("W"), Term.Name(n.toString())), Type.Name("T") ).toString ) }.asPatch }.asPatch ).asPatch def fix(input: Input): String = { val doc = SyntacticDocument.fromInput(input, ScalaVersion.scala2) val patch = createPatch(doc.tree) scalafix.internal.patch.PatchInternals .syntactic( Map(scalafix.rule.RuleName("RefinedTypeCompat") -> patch), doc, false ).fixed } }
あとは細かい部分省略しますが、sbtのsourceGeneratorsからこれを呼び出し、とします。 上記は、2.13や3のコードから、Scala 2.12用のshapeless使ったコードを生成するパターンです。 まぁ逆も原理上は可能でしょうが、2.12が将来消えること考えると、普通はこちらの方法で良いでしょう。
他にも、もっといい案あったら教えてください。
後でrefinedにissueかpull reqで 「どちらかの方法でコードの重複消していい感じにしませんか?」 って提案するかもしれません。
追記: