Skip to content

Commit c151cc4

Browse files
committed
Applier and Unapplier typeclasses
1 parent 06e9f37 commit c151cc4

File tree

4 files changed

+184
-4
lines changed

4 files changed

+184
-4
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.avsystem.commons
2+
package misc
3+
4+
/**
5+
* Typeclass which captures case class `apply` method in a raw form that takes untyped sequence of arguments.
6+
*/
7+
trait Applier[T] {
8+
def apply(rawValues: Seq[Any]): T
9+
}
10+
object Applier {
11+
implicit def materialize[T]: Applier[T] = macro macros.misc.MiscMacros.applier[T]
12+
}
13+
14+
/**
15+
* Typeclass which captures case class `unapply`/`unapplySeq` method in a raw form that returns
16+
* untyped sequence of values.
17+
*/
18+
trait Unapplier[T] {
19+
def unapply(value: T): Seq[Any]
20+
}
21+
object Unapplier {
22+
implicit def materialize[T]: Unapplier[T] = macro macros.misc.MiscMacros.unapplier[T]
23+
}
24+
25+
class ProductUnapplier[T <: Product] extends Unapplier[T] {
26+
def unapply(value: T): Seq[Any] = value.productIterator.toArray[Any]
27+
}
28+
abstract class ProductApplierUnapplier[T <: Product] extends ProductUnapplier[T] with ApplierUnapplier[T]
29+
30+
trait ApplierUnapplier[T] extends Applier[T] with Unapplier[T]
31+
object ApplierUnapplier {
32+
implicit def materialize[T]: ApplierUnapplier[T] = macro macros.misc.MiscMacros.applierUnapplier[T]
33+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.avsystem.commons
2+
package misc
3+
4+
import org.scalactic.source.Position
5+
import org.scalatest.FunSuite
6+
7+
case class Empty()
8+
case class Single(str: String)
9+
case class Multiple(str: String, int: Int)
10+
case class Varargs(str: String, ints: Int*)
11+
case class Generic[T](str: String, value: T)
12+
13+
case class Over22(
14+
p01: String = "01", p02: String = "02", p03: String = "03", p04: String = "04", p05: String = "05",
15+
p06: String = "06", p07: String = "07", p08: String = "08", p09: String = "09", p10: String = "10",
16+
p11: String = "11", p12: String = "12", p13: String = "13", p14: String = "13", p15: String = "15",
17+
p16: String = "16", p17: String = "17", p18: String = "18", p19: String = "18", p20: String = "18",
18+
p21: String = "21", p22: String = "22", p23: String = "23", p24: String = "24", p25: String = "25"
19+
)
20+
21+
class Custom(val str: String, val i: Int) {
22+
override def hashCode(): Int = (str, i).hashCode()
23+
override def equals(obj: Any): Boolean = obj match {
24+
case c: Custom => str == c.str && i == c.i
25+
case _ => false
26+
}
27+
}
28+
object Custom {
29+
def apply(str: String, i: Int): Custom = new Custom(str, i)
30+
def unapply(custom: Custom): Opt[(String, Int)] = Opt((custom.str, custom.i))
31+
}
32+
33+
class ApplierUnapplierTest extends FunSuite {
34+
def roundtrip[T](value: T)(implicit
35+
applier: Applier[T], unapplier: Unapplier[T], applierUnapplier: ApplierUnapplier[T], pos: Position
36+
): Unit = {
37+
assert(value == applier(unapplier.unapply(value)))
38+
assert(value == applierUnapplier(applierUnapplier.unapply(value)))
39+
}
40+
41+
test("no params") {
42+
roundtrip(Empty())
43+
}
44+
test("single param") {
45+
roundtrip(Single(""))
46+
}
47+
test("multiple params") {
48+
roundtrip(Multiple("", 42))
49+
}
50+
test("varargs") {
51+
roundtrip(Varargs("", 1, 2, 3))
52+
}
53+
test("generic") {
54+
roundtrip(Generic("a", "b"))
55+
}
56+
test("more than 22 params") {
57+
roundtrip(Over22())
58+
}
59+
test("custom") {
60+
roundtrip(Custom("", 42))
61+
}
62+
}

commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,14 @@ trait MacroCommons { bundle =>
4949
final val NotInheritedFromSealedTypes = getType(tq"$CommonsPkg.annotation.NotInheritedFromSealedTypes")
5050
final val SeqCompanionSym = typeOf[scala.collection.Seq.type].termSymbol
5151
final val PositionedAT = getType(tq"$CommonsPkg.annotation.positioned")
52-
final val AnnotationType = getType(tq"$ScalaPkg.annotation.Annotation")
5352
final val ImplicitNotFoundAT = getType(tq"$ScalaPkg.annotation.implicitNotFound")
5453

5554
final val NothingTpe: Type = typeOf[Nothing]
5655
final val StringPFTpe: Type = typeOf[PartialFunction[String, Any]]
5756
final val BIterableTpe: Type = typeOf[Iterable[Any]]
5857
final val BIndexedSeqTpe: Type = typeOf[IndexedSeq[Any]]
58+
final val ProductTpe: Type = typeOf[Product]
59+
final val AnnotationTpe: Type = typeOf[scala.annotation.Annotation]
5960

6061
final val PartialFunctionClass: Symbol = StringPFTpe.typeSymbol
6162
final val BIterableClass: Symbol = BIterableTpe.typeSymbol
@@ -801,7 +802,9 @@ trait MacroCommons { bundle =>
801802
* @param unapply companion object'a unapply method or `NoSymbol` for case class with more than 22 fields
802803
* @param params parameters with trees evaluating to default values (or `EmptyTree`s)
803804
*/
804-
case class ApplyUnapply(apply: Symbol, unapply: Symbol, params: List[(TermSymbol, Tree)])
805+
case class ApplyUnapply(apply: Symbol, unapply: Symbol, params: List[(TermSymbol, Tree)]) {
806+
def standardCaseClass: Boolean = apply.isConstructor
807+
}
805808

806809
def applyUnapplyFor(tpe: Type): Option[ApplyUnapply] =
807810
typedCompanionOf(tpe).flatMap(comp => applyUnapplyFor(tpe, comp))
@@ -847,7 +850,8 @@ trait MacroCommons { bundle =>
847850
val applicableResults = applyUnapplyPairs.flatMap {
848851
case (apply, unapply) if typeParamsMatch(apply, unapply) =>
849852
val constructor =
850-
if (caseClass && apply.isSynthetic) primaryConstructorOf(dtpe)
853+
if (caseClass && apply.isSynthetic && unapply.isSynthetic)
854+
primaryConstructorOf(dtpe)
851855
else NoSymbol
852856

853857
val applySig =

commons-macros/src/main/scala/com/avsystem/commons/macros/misc/MiscMacros.scala

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,87 @@ class MiscMacros(ctx: blackbox.Context) extends AbstractMacroCommons(ctx) {
346346

347347
def posPoint: Tree =
348348
q"${c.enclosingPosition.point}"
349+
350+
def applyUnapplyOrFail(tpe: Type): ApplyUnapply =
351+
applyUnapplyFor(tpe).getOrElse(abort(
352+
s"$tpe is not a case class or case-class like type: no matching apply/unapply pair found"))
353+
354+
def applyBody(rawValuesName: TermName, tpe: Type, au: ApplyUnapply): Tree = {
355+
val args = au.params.zipWithIndex.map { case ((param, _), idx) =>
356+
val res = q"$rawValuesName($idx).asInstanceOf[${actualParamType(param.typeSignature)}]"
357+
if (isRepeated(param)) q"$res: _*" else res
358+
}
359+
if (au.standardCaseClass) q"new $tpe(..$args)"
360+
else q"${replaceCompanion(typedCompanionOf(tpe).getOrElse(EmptyTree))}.apply[..${tpe.typeArgs}](..$args)"
361+
}
362+
363+
def unapplyBody(valueName: TermName, tpe: Type, au: ApplyUnapply): Tree = {
364+
if (au.standardCaseClass) q"$ScalaPkg.Array(..${au.params.map { case (param, _) => q"$valueName.$param" }})"
365+
else {
366+
val safeCompanion = replaceCompanion(typedCompanionOf(tpe).getOrElse(EmptyTree))
367+
val unapplyRes = q"$safeCompanion.${au.unapply}[..${tpe.typeArgs}]($valueName)"
368+
au.params match {
369+
case Nil => q"$ScalaPkg.Seq.empty[$ScalaPkg.Any]"
370+
case List(_) => q"$ScalaPkg.Array($unapplyRes.get)"
371+
case _ =>
372+
val resName = c.freshName(TermName("res"))
373+
val elems = au.params.indices.map(i => q"$resName.${TermName(s"_${i + 1}")}")
374+
q"""
375+
val $resName = $unapplyRes.get
376+
$ScalaPkg.Array[$ScalaPkg.Any](..$elems)
377+
"""
378+
}
379+
}
380+
}
381+
382+
def applier[T: WeakTypeTag]: Tree = {
383+
val tpe = weakTypeOf[T].dealias
384+
val rawValuesName = c.freshName(TermName("rawValues"))
385+
q"""
386+
new $CommonsPkg.misc.Applier[$tpe] {
387+
def apply($rawValuesName: $ScalaPkg.Seq[$ScalaPkg.Any]): $tpe =
388+
${applyBody(rawValuesName, tpe, applyUnapplyOrFail(tpe))}
389+
}
390+
"""
391+
}
392+
393+
def unapplier[T: WeakTypeTag]: Tree = {
394+
val tpe = weakTypeOf[T].dealias
395+
val valueName = c.freshName(TermName("value"))
396+
val au = applyUnapplyOrFail(tpe)
397+
if (au.standardCaseClass && tpe <:< ProductTpe)
398+
q"new $CommonsPkg.misc.ProductUnapplier[$tpe]"
399+
else
400+
q"""
401+
new $CommonsPkg.misc.Unapplier[$tpe] {
402+
def unapply($valueName: $tpe): $ScalaPkg.Seq[$ScalaPkg.Any] =
403+
${unapplyBody(valueName, tpe, au)}
404+
}
405+
"""
406+
}
407+
408+
def applierUnapplier[T: WeakTypeTag]: Tree = {
409+
val tpe = weakTypeOf[T].dealias
410+
val rawValuesName = c.freshName(TermName("rawValues"))
411+
val valueName = c.freshName(TermName("value"))
412+
val au = applyUnapplyOrFail(tpe)
413+
if (au.standardCaseClass && tpe <:< ProductTpe)
414+
q"""
415+
new $CommonsPkg.misc.ProductApplierUnapplier[$tpe] {
416+
def apply($rawValuesName: $ScalaPkg.Seq[$ScalaPkg.Any]): $tpe =
417+
${applyBody(rawValuesName, tpe, au)}
418+
}
419+
"""
420+
else
421+
q"""
422+
new $CommonsPkg.misc.ApplierUnapplier[$tpe] {
423+
def apply($rawValuesName: $ScalaPkg.Seq[$ScalaPkg.Any]): $tpe =
424+
${applyBody(rawValuesName, tpe, au)}
425+
def unapply($valueName: $tpe): $ScalaPkg.Seq[$ScalaPkg.Any] =
426+
${unapplyBody(valueName, tpe, au)}
427+
}
428+
"""
429+
}
349430
}
350431

351432
class WhiteMiscMacros(ctx: whitebox.Context) extends AbstractMacroCommons(ctx) {
@@ -392,7 +473,7 @@ class WhiteMiscMacros(ctx: whitebox.Context) extends AbstractMacroCommons(ctx) {
392473
case DefaultValueMethod(p) => p
393474
case p => p
394475
}
395-
if (param.owner.owner.asType.toType <:< AnnotationType && findAnnotation(param, InferAT).nonEmpty)
476+
if (param.owner.owner.asType.toType <:< AnnotationTpe && findAnnotation(param, InferAT).nonEmpty)
396477
q"""throw new $ScalaPkg.NotImplementedError("infer.value")"""
397478
else
398479
abort(s"infer.value can be only used as default value of @infer annotation parameters")

0 commit comments

Comments
 (0)