Scala宏/乐园案例类apply方法

huangapple go评论81阅读模式
英文:

Scala macros/paradise case class apply method

问题

这是你要翻译的代码部分:

  1. I'm working on Scala 2.12.17.
  2. Let's say I've a bunch of case classes:
  3. case class TestOne(one: String)
  4. case class TestTwo(one: String, two: String)
  5. case class TestThree(one: String, two: String, three: String)
  6. I also have these types:
  7. trait Data {
  8. val a: Int
  9. }
  10. case class DoubleInt(a: Int, b: Int) extends Data
  11. case class SingleInt(a: Int) extends Data
  12. And this function which converts `Data` objects to `String`:
  13. def loadData(input: Data): String = {
  14. input.a.toString
  15. }
  16. What I'm looking forward is to pass `Data` object(s) to my case class's apply method, then the apply method would use `loadData` function in order to convert each passed `Data` object into a `String` to make an instance of my case class. E.g:
  17. val dataOne: Data = SingleInt(1)
  18. val dataTwo: Data = DoubleInt(1, 2)
  19. val testOne = TestOne(dataOne)
  20. val testTwo = TestTwo(dataOne, dataTwo)
  21. val testThree = TestOne(dataOne, dataTwo, dataOne)
  22. Basically, `TestOne` `apply` method would be:
  23. def apply(one: Data): TestOne = {
  24. new TestOne(loadData(one))
  25. }
  26. `TestTwo` `apply` method would be:
  27. def apply(one: Data, two: Data): TestTwo = {
  28. new TestTwo(loadData(one), loadData(two))
  29. }
  30. Is there any way to programmatically generate those apply methods at compile time?
  31. I thought that macros or paradise annotations would be useful for this use case, but I'm too inexperienced with these topics to even know where to start :/
英文:

I'm working on Scala 2.12.17.

Let's say I've a bunch of case classes :

  1. case class TestOne(one: String)
  2. case class TestTwo(one: String, two: String)
  3. case class TestThree(one: String, two: String, three: String)

I also have these types :

  1. trait Data{
  2. val a: Int
  3. }
  4. case class DoubleInt(a: Int, b: Int) extends Data
  5. case class SingleInt(a: Int) extends Data

And this function which converts Data objects to String :

  1. def loadData(input: Data): String = {
  2. input.a.toString
  3. }

What I'm looking forward is to pass Data object(s) to my case classe's apply method, then the apply method would use loadData function in order to convert each passed Data object into a String to make an instance of my case class. E.g :

  1. val dataOne: Data = SingleInt(1)
  2. val dataTwo: Data = DoubleInt(1, 2)
  3. val testOne = TestOne(dataOne)
  4. val testTwo = TestTwo(dataOne, dataTwo)
  5. val testThree = TestOne(dataOne, dataTwo, dataOne)

Basically, TestOne apply method would be :

  1. def apply(one: Data): TestOne = {
  2. new TestOne(loadData(one))
  3. }

TestTwo apply method would be :

  1. def apply(one: Data, two: Data): TestTwo= {
  2. new TestTwo(loadData(one), loadData(two))
  3. }

Is there any way to programatically generate those apply methods at compile time ?

I thought that macros or paradise annotations would be useful for this use case, but I'm too unexperienced with these topics to even know where to start :/

答案1

得分: 1

  1. Should
  2. val testThree = TestOne(dataOne, dataTwo, dataOne)
  3. be `val testThree = TestThree(dataOne, dataTwo, dataOne)`?
  4. So you'd like to replace

val testOne = TestOne(loadData(dataOne))
val testTwo = TestTwo(loadData(dataOne), loadData(dataTwo))
val testThree = TestThree(loadData(dataOne), loadData(dataTwo), loadData(dataOne))

  1. with just

val testOne = TestOne(dataOne)
val testTwo = TestTwo(dataOne, dataTwo)
val testThree = TestThree(dataOne, dataTwo, dataOne)

  1. Mapping over a tuple or case class is a standard task, for example, for [Shapeless][1]
  2. ```scala
  3. // libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
  4. import shapeless.poly.->
  5. import shapeless.syntax.std.tuple._
  6. object loadDataPoly extends (Data -> String)(loadData)
  7. val testOne = TestOne(loadData(dataOne))
  8. val testTwo = TestTwo.tupled((dataOne, dataTwo).map(loadDataPoly))
  9. val testThree = TestThree.tupled((dataOne, dataTwo, dataOne).map(loadDataPoly))

If you want to hide load at all, you can define a generic method

  1. import shapeless.{Generic, HList}
  2. import shapeless.ops.traversable.FromTraversable
  3. def make[A <: Product] = new PartiallyAppliedMake[A]
  4. class PartiallyAppliedMake[A <: Product] {
  5. def apply[L <: HList](data: Data*)(implicit
  6. generic: Generic.Aux[A, L],
  7. fromTraversable: FromTraversable[L]
  8. ): A = generic.from(fromTraversable(data.map(loadData)).get)
  9. }
  10. val testOne = make[TestOne](dataOne)
  11. val testTwo = make[TestTwo](dataOne, dataTwo)
  12. val testThree = make[TestThree](dataOne, dataTwo, dataOne)
  13. val testThree_ = make[TestThree](dataOne, dataTwo, dataOne, dataOne) // fails at runtime

If you want make to fail at compile time if the number of arguments is incorrect, then the definition is a little more complicated

  1. import shapeless.{Generic, HList, Nat}
  2. import shapeless.ops.hlist.{Mapper, Length, Fill}
  3. import shapeless.ops.function.FnFromProduct
  4. def make[TestClass <: Product] = new PartiallyAppliedMake[TestClass]
  5. class PartiallyAppliedMake[TestClass <: Product] {
  6. def apply[StringHList <: HList, N <: Nat, DataHList <: HList]()(implicit
  7. generic: Generic.Aux[TestClass, StringHList],
  8. length: Length.Aux[StringHList, N],
  9. fill: Fill.Aux[N, Data, DataHList],
  10. mapper: Mapper.Aux[loadDataPoly.type, DataHList, StringHList],
  11. fnFromProduct: FnFromProduct[DataHList => TestClass]
  12. ): fnFromProduct.Out =
  13. fnFromProduct((l: DataHList) => generic.from(mapper(l)))
  14. }
  15. val testOne = make[TestOne]().apply(dataOne)
  16. val testTwo = make[TestTwo]().apply(dataOne, dataTwo)
  17. val testThree = make[TestThree]().apply(dataOne, dataTwo, dataOne)
  18. val testThree_ = make[TestThree]().apply(dataOne, dataTwo, dataOne, dataOne) // fails at compile time

Or you can define a [def macro][2]

  1. // libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value
  2. import scala.language.experimental.macros
  3. import scala.reflect.macros.blackbox
  4. def make[A](data: Data*): A = macro makeImpl[A]
  5. def makeImpl[A: c.WeakTypeTag](c: blackbox.Context)(data: c.Tree*): c.Tree = {
  6. import c.universe._
  7. val A = weakTypeOf[A]
  8. val strs = data.map(t => q"loadData($t)")
  9. q"new $A(..$strs)"
  10. }
  1. // in a different subproject
  2. val testOne = make[TestOne](dataOne) // TestOne(1)
  3. val testTwo = make[TestTwo](dataOne, dataTwo) // TestTwo(1,1)
  4. val testThree = make[TestThree](dataOne, dataTwo, dataOne) // TestThree(1,1,1)
  5. val testThree_ = make[TestThree](dataOne, dataTwo, dataOne, dataOne) // doesn't compile: too many arguments (found 4, expected 3) ...

But if you really want to generate apply methods in companion objects, you can define [macro annotation][3].

  1. // addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full) // Scala 2.12
  2. import scala.annotation.{StaticAnnotation, compileTimeOnly}
  3. import scala.language.experimental.macros
  4. import scala.reflect.macros.blackbox
  5. @compileTimeOnly("enable macro annotations")
  6. class generateApply extends StaticAnnotation {
  7. def macroTransform(annottees: Any*): Any = macro GenerateApplyMacro.macroTransformImpl
  8. }
  9. object GenerateApplyMacro {
  10. def macroTransformImpl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
  11. import c.universe._
  12. def modify(cls: ClassDef, obj: ModuleDef): Tree = (cls, obj) match {
  13. case (
  14. q"$_ class $tpname[..$tparams] $_(...$paramss) extends { ..$_ } with ..$_ { $_ => ..$_ }",
  15. q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }"
  16. ) =>
  17. val paramss1 = paramss.map(_.map {
  18. case q"$mods val $tname: $_ = $_" => q"$mods val $tname: Data"
  19. })
  20. val argss = paramss.map(_.map {
  21. case q"$_ val $tname: $_ = $_" => q"loadData($tname)"
  22. })
  23. val targs = tparams.map {
  24. case q"$_ type $tpname[..$_] = $tpt" => tq"$tpname"
  25. }
  26. q"""
  27. $cls
  28. $mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
  29. def apply[..$tparams](...$paramss1): $tpname[..$targs] = {
  30. new $tpname[..$targs](...$argss)
  31. }
  32. ..$body
  33. }
  34. """
  35. }
  36. annottees match {
  37. case (cls: ClassDef) :: (obj: ModuleDef) :: Nil => modify(cls, obj)
  38. case (cls: ClassDef) :: Nil => modify(cls, q"object ${cls.name.toTermName}")
  39. }
  40. }
  41. }
  1. // in a different subproject
  2. @generateApply case class TestOne(one: String)
  3. @generateApply case class TestTwo(one: String
  4. <details>
  5. <summary>英文:</summary>
  6. Should
  7. val testThree = TestOne(dataOne, dataTwo, dataOne)
  8. be `val testThree = TestThree(dataOne, dataTwo, dataOne)`?
  9. So you&#39;d like to replace

val testOne = TestOne(loadData(dataOne))
val testTwo = TestTwo(loadData(dataOne), loadData(dataTwo))
val testThree = TestThree(loadData(dataOne), loadData(dataTwo), loadData(dataOne))

  1. with just

val testOne = TestOne(dataOne)
val testTwo = TestTwo(dataOne, dataTwo)
val testThree = TestThree(dataOne, dataTwo, dataOne)

  1. Mapping over a tuple or case class is a standard task for example for [Shapeless][1]

// libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
import shapeless.poly.->
import shapeless.syntax.std.tuple._

object loadDataPoly extends (Data -> String)(loadData)

val testOne = TestOne(loadData(dataOne))
val testTwo = TestTwo.tupled((dataOne, dataTwo).map(loadDataPoly))
val testThree = TestThree.tupled((dataOne, dataTwo, dataOne).map(loadDataPoly))

  1. If you want to hide `load` at all you can define a generic method

import shapeless.{Generic, HList}
import shapeless.ops.traversable.FromTraversable

def make[A <: Product] = new PartiallyAppliedMake[A]

class PartiallyAppliedMake[A <: Product] {
def apply[L <: HList](data: Data*)(implicit
generic: Generic.Aux[A, L],
fromTraversable: FromTraversable[L]
): A = generic.from(fromTraversable(data.map(loadData)).get)
}

val testOne = makeTestOne
val testTwo = make[TestTwo](dataOne, dataTwo)
val testThree = make[TestThree](dataOne, dataTwo, dataOne)
val testThree_ = make[TestThree](dataOne, dataTwo, dataOne, dataOne) // fails at runtime

  1. If you want `make` to fail at compile time if the number of arguments is incorrect then the definition is a little more complicated

import shapeless.{Generic, HList, Nat}
import shapeless.ops.hlist.{Mapper, Length, Fill}
import shapeless.ops.function.FnFromProduct

def make[TestClass <: Product] = new PartillyAppliedMake[TestClass]

class PartillyAppliedMake[TestClass <: Product] {
def applyStringHList <: HList, N <: Nat, DataHList <: HList(implicit
generic: Generic.Aux[TestClass, StringHList],
length: Length.Aux[StringHList, N],
fill: Fill.Aux[N, Data, DataHList],
mapper: Mapper.Aux[loadDataPoly.type, DataHList, StringHList],
fnFromProduct: FnFromProduct[DataHList => TestClass]
): fnFromProduct.Out =
fnFromProduct((l: DataHList) => generic.from(mapper(l)))
}

val testOne = makeTestOne.apply(dataOne)
val testTwo = makeTestTwo.apply(dataOne, dataTwo)
val testThree = makeTestThree.apply(dataOne, dataTwo, dataOne)
val testThree_ = makeTestThree.apply(dataOne, dataTwo, dataOne, dataOne) // fails at compile time

  1. Or you can define a [def macro][2]

// libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

def make[A](data: Data*): A = macro makeImpl[A]

def makeImpl[A: c.WeakTypeTag](c: blackbox.Context)(data: c.Tree*): c.Tree = {
import c.universe._
val A = weakTypeOf[A]
val strs = data.map(t => q"loadData($t)")
q"new $A(..$strs)"
}

// in a different subproject

val testOne = makeTestOne // TestOne(1)
val testTwo = make[TestTwo](dataOne, dataTwo) // TestTwo(1,1)
val testThree = make[TestThree](dataOne, dataTwo, dataOne) // TestThree(1,1,1)
val testThree_ = make[TestThree](dataOne, dataTwo, dataOne, dataOne) // doesn't compile: too many arguments (found 4, expected 3) ...

  1. // scalacOptions += &quot;-Ymacro-debug-lite&quot;

//scalac: new App.TestOne(loadData(App.this.dataOne))
//scalac: new App.TestTwo(loadData(App.this.dataOne), loadData(App.this.dataTwo))
//scalac: new App.TestThree(loadData(App.this.dataOne), loadData(App.this.dataTwo), loadData(App.this.dataOne))

  1. But if you really want to generate `apply` methods in companion objects you can define [macro annotaion][3] (settings: https://stackoverflow.com/questions/74497076/auto-generate-companion-object-for-case-class-in-scala)

// addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full) // Scala 2.12
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("enable macro annotations")
class generateApply extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro GenerateApplyMacro.macroTransformImpl
}
object GenerateApplyMacro {
def macroTransformImpl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
import c.universe._

  1. def modify(cls: ClassDef, obj: ModuleDef): Tree = (cls, obj) match {
  2. case (
  3. q&quot;$_ class $tpname[..$tparams] $_(...$paramss) extends { ..$_ } with ..$_ { $_ =&gt; ..$_ }&quot;,
  4. q&quot;$mods object $tname extends { ..$earlydefns } with ..$parents { $self =&gt; ..$body }&quot;
  5. ) =&gt;
  6. val paramss1 = paramss.map(_.map {
  7. case q&quot;$mods val $tname: $_ = $_&quot; =&gt; q&quot;$mods val $tname: Data&quot;
  8. })
  9. val argss = paramss.map(_.map {
  10. case q&quot;$_ val $tname: $_ = $_&quot; =&gt; q&quot;loadData($tname)&quot;
  11. })
  12. val targs = tparams.map {
  13. case q&quot;$_ type $tpname[..$_] = $tpt&quot; =&gt; tq&quot;$tpname&quot;
  14. }
  15. q&quot;&quot;&quot;
  16. $cls
  17. $mods object $tname extends { ..$earlydefns } with ..$parents { $self =&gt;
  18. def apply[..$tparams](...$paramss1): $tpname[..$targs] = {
  19. new $tpname[..$targs](...$argss)
  20. }
  21. ..$body
  22. }
  23. &quot;&quot;&quot;
  24. }
  25. annottees match {
  26. case (cls: ClassDef) :: (obj: ModuleDef) :: Nil =&gt; modify(cls, obj)
  27. case (cls: ClassDef) :: Nil =&gt; modify(cls, q&quot;object ${cls.name.toTermName}&quot;)
  28. }

}
}

// in a different subproject

@generateApply case class TestOne(one: String)
@generateApply case class TestTwo(one: String, two: String)
@generateApply case class TestThree(one: String, two: String, three: String)

val testOne = TestOne(dataOne) // TestOne(1)
val testTwo = TestTwo(dataOne, dataTwo) // TestTwo(1,1)
val testThree = TestThree(dataOne, dataTwo, dataOne) // TestThree(1,1,1)

//scalac: {
// case class TestOne extends scala.Product with scala.Serializable {
// <caseaccessor> <paramaccessor> val one: String = _;
// def <init>(one: String) = {
// super.<init>();
// ()
// }
// };
// object TestOne extends scala.AnyRef {
// def <init>() = {
// super.<init>();
// ()
// };
// def apply(one: Data): TestOne = new TestOne(loadData(one))
// };
// ()
//}

//scalac: {
// case class TestTwo extends scala.Product with scala.Serializable {
// <caseaccessor> <paramaccessor> val one: String = _;
// <caseaccessor> <paramaccessor> val two: String = _;
// def <init>(one: String, two: String) = {
// super.<init>();
// ()
// }
// };
// object TestTwo extends scala.AnyRef {
// def <init>() = {
// super.<init>();
// ()
// };
// def apply(one: Data, two: Data): TestTwo = new TestTwo(loadData(one), loadData(two))
// };
// ()
//}

//scalac: {
// case class TestThree extends scala.Product with scala.Serializable {
// <caseaccessor> <paramaccessor> val one: String = _;
// <caseaccessor> <paramaccessor> val two: String = _;
// <caseaccessor> <paramaccessor> val three: String = _;
// def <init>(one: String, two: String, three: String) = {
// super.<init>();
// ()
// }
// };
// object TestThree extends scala.AnyRef {
// def <init>() = {
// super.<init>();
// ()
// };
// def apply(one: Data, two: Data, three: Data): TestThree = new TestThree(loadData(one), loadData(two), loadData(three))
// };
// ()
//}

  1. [1]: https://github.com/milessabin/shapeless
  2. [2]: https://docs.scala-lang.org/overviews/macros/overview.html
  3. [3]: https://docs.scala-lang.org/overviews/macros/annotations.html
  4. </details>

huangapple
  • 本文由 发表于 2023年3月15日 20:01:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/75744437.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定