From 0c3535d1f5e0e8cc0d99c09b524d906ed83d32de Mon Sep 17 00:00:00 2001 From: Mateusz Kubuszok Date: Thu, 18 Jul 2024 16:46:55 +0200 Subject: [PATCH] Write an example implementation of a library with Chimney derivarion engine but based on a different type-class --- .../compiletime/DerivationEngine.scala | 22 +++++- .../MyTypeClassCompanionPlatform.scala | 8 +++ .../MyTypeClassDerivationPlatform.scala | 57 +++++++++++++++ .../example/macros/MyTypeClassMacros.scala | 14 ++++ .../MyTypeClassCompanionPlatform.scala | 6 ++ .../MyTypeClassDerivationPlatform.scala | 50 +++++++++++++ .../example/internal/MyTypeClassMacros.scala | 14 ++++ .../chimney/example/MyTypeClass.scala | 7 ++ .../internal/MyTypeClassDerivation.scala | 72 +++++++++++++++++++ 9 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 chimney-engine/src/test/scala-2/io/scalaland/chimney/example/MyTypeClassCompanionPlatform.scala create mode 100644 chimney-engine/src/test/scala-2/io/scalaland/chimney/example/macros/MyTypeClassDerivationPlatform.scala create mode 100644 chimney-engine/src/test/scala-2/io/scalaland/chimney/example/macros/MyTypeClassMacros.scala create mode 100644 chimney-engine/src/test/scala-3/io/scalaland/chimney/example/MyTypeClassCompanionPlatform.scala create mode 100644 chimney-engine/src/test/scala-3/io/scalaland/chimney/example/internal/MyTypeClassDerivationPlatform.scala create mode 100644 chimney-engine/src/test/scala-3/io/scalaland/chimney/example/internal/MyTypeClassMacros.scala create mode 100644 chimney-engine/src/test/scala/io/scalaland/chimney/example/MyTypeClass.scala create mode 100644 chimney-engine/src/test/scala/io/scalaland/chimney/example/internal/MyTypeClassDerivation.scala diff --git a/chimney-engine/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationEngine.scala b/chimney-engine/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationEngine.scala index 58708c276..fd50c6c18 100644 --- a/chimney-engine/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationEngine.scala +++ b/chimney-engine/src/main/scala/io/scalaland/chimney/internal/compiletime/DerivationEngine.scala @@ -87,7 +87,7 @@ package io.scalaland.chimney.internal.compiletime * * def deriveBody(src: c.Expr[From]): c.Expr[To] = { * val cfg = TransformerConfiguration() // customize, read config with DSL etc - * val context = TransformationContext.ForTotal(src, cfg) + * val context = TransformationContext.ForTotal.create(src, cfg) * * deriveFinalTransformationResultExpr(context).toEither.fold( * derivationErrors => reportError(derivationErrors.toString), // customize @@ -118,7 +118,7 @@ package io.scalaland.chimney.internal.compiletime * * def deriveBody(src: Expr[From]): Expr[To] = { * val cfg = TransformerConfiguration() // customize, read config with DSL etc - * val context = TransformationContext.ForTotal(src, cfg) + * val context = TransformationContext.ForTotal.create(src, cfg) * * deriveFinalTransformationResultExpr(context).toEither.fold( * derivationErrors => reportError(derivationErrors.toString), // customize @@ -150,4 +150,20 @@ trait DerivationEngine with derivation.transformer.integrations.PartiallyBuildIterables with derivation.transformer.integrations.TotallyBuildIterables with derivation.transformer.integrations.TotallyOrPartiallyBuildIterables - with derivation.transformer.rules.TransformationRules + with derivation.transformer.rules.TransformationRules { + + type DerivationResult[+A] = io.scalaland.chimney.internal.compiletime.DerivationResult[A] + val DerivationResult = io.scalaland.chimney.internal.compiletime.DerivationResult + + /** Adapts TransformationExpr[To] to expected type of transformation */ + def deriveFinalTransformationResultExpr[From, To](implicit + ctx: TransformationContext[From, To] + ): DerivationResult[Expr[ctx.Target]] = + DerivationResult.log(s"Start derivation with context: $ctx") >> + deriveTransformationResultExpr[From, To] + .map { transformationExpr => + ctx.fold(_ => transformationExpr.ensureTotal.asInstanceOf[Expr[ctx.Target]])(_ => + transformationExpr.ensurePartial.asInstanceOf[Expr[ctx.Target]] + ) + } +} diff --git a/chimney-engine/src/test/scala-2/io/scalaland/chimney/example/MyTypeClassCompanionPlatform.scala b/chimney-engine/src/test/scala-2/io/scalaland/chimney/example/MyTypeClassCompanionPlatform.scala new file mode 100644 index 000000000..6b08612f8 --- /dev/null +++ b/chimney-engine/src/test/scala-2/io/scalaland/chimney/example/MyTypeClassCompanionPlatform.scala @@ -0,0 +1,8 @@ +package io.scalaland.chimney.example + +import scala.language.experimental.macros + +trait MyTypeClassCompanionPlatform { this: MyTypeClass.type => + + def deriving[From, To]: MyTypeClass[From, To] = macro internal.MyTypeClassMacros.derivingImpl[From, To] +} diff --git a/chimney-engine/src/test/scala-2/io/scalaland/chimney/example/macros/MyTypeClassDerivationPlatform.scala b/chimney-engine/src/test/scala-2/io/scalaland/chimney/example/macros/MyTypeClassDerivationPlatform.scala new file mode 100644 index 000000000..698aee523 --- /dev/null +++ b/chimney-engine/src/test/scala-2/io/scalaland/chimney/example/macros/MyTypeClassDerivationPlatform.scala @@ -0,0 +1,57 @@ +package io.scalaland.chimney.example.internal + +import io.scalaland.chimney.example.MyTypeClass +import io.scalaland.chimney.example.internal.MyTypeClassDerivation +import io.scalaland.chimney.internal.compiletime.{DerivationEnginePlatform, StandardRules} + +trait MyTypeClassDerivationPlatform extends DerivationEnginePlatform with MyTypeClassDerivation with StandardRules { + + // in Scala-2-specific code, remember to import content of the universe + import c.universe.* + + protected object MyTypes extends MyTypesModule { + + import Type.platformSpecific.* + + object MyTypeClass extends MyTypeClassModule { + def apply[From: Type, To: Type]: Type[MyTypeClass[From, To]] = weakTypeTag[MyTypeClass[From, To]] + def unapply[A](A: Type[A]): Option[(??, ??)] = + A.asCtor[MyTypeClass[?, ?]].map(A0 => A0.param(0) -> A0.param(1)) // utility from Type.platformSpecific.* + } + } + + protected object MyExprs extends MyExprsModule { + + def callMyTypeClass[From: Type, To: Type](tc: Expr[MyTypeClass[From, To]], from: Expr[From]): Expr[To] = + c.Expr[To](q"""$tc.convert($from)""") + + def createTypeClass[From: Type, To: Type](body: Expr[From] => Expr[To]): Expr[MyTypeClass[From, To]] = { + val name = ExprPromise.platformSpecific.freshTermName("from") + // remember to use full qualified names in Scala 2 macros!!! + c.Expr[MyTypeClass[From, To]]( + q""" + new _root_.io.scalaland.chimney.example.MyTypeClass[${Type[From]}, ${Type[To]}] { + def convert($name: ${Type[From]}): ${Type[To]} = ${body(c.Expr[From](q"$name"))} + } + """ + ) + } + } + + final override protected val rulesAvailableForPlatform: List[Rule] = List( + MyTypeClassImplicitRule, // replacing TransformImplicitRule + TransformSubtypesRule, + TransformToSingletonRule, + TransformOptionToOptionRule, + TransformPartialOptionToNonOptionRule, + TransformToOptionRule, + TransformValueClassToValueClassRule, + TransformValueClassToTypeRule, + TransformTypeToValueClassRule, + TransformEitherToEitherRule, + TransformMapToMapRule, + TransformIterableToIterableRule, + TransformProductToProductRule, + TransformSealedHierarchyToSealedHierarchyRule + ) +} diff --git a/chimney-engine/src/test/scala-2/io/scalaland/chimney/example/macros/MyTypeClassMacros.scala b/chimney-engine/src/test/scala-2/io/scalaland/chimney/example/macros/MyTypeClassMacros.scala new file mode 100644 index 000000000..b036adffb --- /dev/null +++ b/chimney-engine/src/test/scala-2/io/scalaland/chimney/example/macros/MyTypeClassMacros.scala @@ -0,0 +1,14 @@ +package io.scalaland.chimney.example.internal + +import io.scalaland.chimney.example.MyTypeClass + +import scala.reflect.macros.blackbox + +// Scala 2 macro bundle +class MyTypeClassMacros(val c: blackbox.Context) extends MyTypeClassDerivationPlatform { + + // Scala 2 is kinda unaware during macro expansion that myTypeClassDerivation takes c.WeakTypeTag, and we need to + // point it out for it, explicitly + def derivingImpl[From: c.WeakTypeTag, To: c.WeakTypeTag]: c.Expr[MyTypeClass[From, To]] = + myTypeClassDerivation[From, To] +} diff --git a/chimney-engine/src/test/scala-3/io/scalaland/chimney/example/MyTypeClassCompanionPlatform.scala b/chimney-engine/src/test/scala-3/io/scalaland/chimney/example/MyTypeClassCompanionPlatform.scala new file mode 100644 index 000000000..c2807e0d2 --- /dev/null +++ b/chimney-engine/src/test/scala-3/io/scalaland/chimney/example/MyTypeClassCompanionPlatform.scala @@ -0,0 +1,6 @@ +package io.scalaland.chimney.example + +trait MyTypeClassCompanionPlatform { this: MyTypeClass.type => + + inline def deriving[From, To] = ${ internal.MyTypeClassMacros.derivingImpl[From, To] } +} diff --git a/chimney-engine/src/test/scala-3/io/scalaland/chimney/example/internal/MyTypeClassDerivationPlatform.scala b/chimney-engine/src/test/scala-3/io/scalaland/chimney/example/internal/MyTypeClassDerivationPlatform.scala new file mode 100644 index 000000000..15a467d54 --- /dev/null +++ b/chimney-engine/src/test/scala-3/io/scalaland/chimney/example/internal/MyTypeClassDerivationPlatform.scala @@ -0,0 +1,50 @@ +package io.scalaland.chimney.example.internal + +import io.scalaland.chimney.example.MyTypeClass +import io.scalaland.chimney.example.internal.MyTypeClassDerivation +import io.scalaland.chimney.internal.compiletime.{DerivationEnginePlatform, StandardRules} + +abstract class MyTypeClassDerivationPlatform(q: scala.quoted.Quotes) + extends DerivationEnginePlatform(q) + with MyTypeClassDerivation + with StandardRules { + + // in Scala-3-specific code, remember to import content of the quotes and quotes.reflect + import q.*, q.reflect.* + + protected object MyTypes extends MyTypesModule { + + object MyTypeClass extends MyTypeClassModule { + def apply[From: Type, To: Type]: Type[MyTypeClass[From, To]] = scala.quoted.Type.of[MyTypeClass[From, To]] + def unapply[A](A: Type[A]): Option[(??, ??)] = A match + case '[MyTypeClass[from, to]] => Some((Type[from].as_??, Type[to].as_??)) + case _ => None + } + } + + protected object MyExprs extends MyExprsModule { + + def callMyTypeClass[From: Type, To: Type](tc: Expr[MyTypeClass[From, To]], from: Expr[From]): Expr[To] = + '{ ${ tc }.convert(${ from }) } + + def createTypeClass[From: Type, To: Type](body: Expr[From] => Expr[To]): Expr[MyTypeClass[From, To]] = + '{ new MyTypeClass[From, To] { def convert(from: From): To = ${ body('{ from }) } } } + } + + final override protected val rulesAvailableForPlatform: List[Rule] = List( + MyTypeClassImplicitRule, // replacing TransformImplicitRule + TransformSubtypesRule, + TransformToSingletonRule, + TransformOptionToOptionRule, + TransformPartialOptionToNonOptionRule, + TransformToOptionRule, + TransformValueClassToValueClassRule, + TransformValueClassToTypeRule, + TransformTypeToValueClassRule, + TransformEitherToEitherRule, + TransformMapToMapRule, + TransformIterableToIterableRule, + TransformProductToProductRule, + TransformSealedHierarchyToSealedHierarchyRule + ) +} diff --git a/chimney-engine/src/test/scala-3/io/scalaland/chimney/example/internal/MyTypeClassMacros.scala b/chimney-engine/src/test/scala-3/io/scalaland/chimney/example/internal/MyTypeClassMacros.scala new file mode 100644 index 000000000..6ed38607e --- /dev/null +++ b/chimney-engine/src/test/scala-3/io/scalaland/chimney/example/internal/MyTypeClassMacros.scala @@ -0,0 +1,14 @@ +package io.scalaland.chimney.example.internal + +import io.scalaland.chimney.example.MyTypeClass + +import scala.quoted.* + +class MyTypeClassMacros(q: Quotes) extends MyTypeClassDerivationPlatform(q) +object MyTypeClassMacros { + + // Scala 3 expect all macros to be methods in objects, no Scala 2's macro-bundle-like equivalents, so we have to + // instantiate a class and call it ourselves + def derivingImpl[From: Type, To: Type](using q: Quotes): Expr[MyTypeClass[From, To]] = + new MyTypeClassMacros(q).myTypeClassDerivation[From, To] +} diff --git a/chimney-engine/src/test/scala/io/scalaland/chimney/example/MyTypeClass.scala b/chimney-engine/src/test/scala/io/scalaland/chimney/example/MyTypeClass.scala new file mode 100644 index 000000000..74376b4bd --- /dev/null +++ b/chimney-engine/src/test/scala/io/scalaland/chimney/example/MyTypeClass.scala @@ -0,0 +1,7 @@ +package io.scalaland.chimney.example + +trait MyTypeClass[From, To] { + + def convert(from: From): To +} +object MyTypeClass extends MyTypeClassCompanionPlatform diff --git a/chimney-engine/src/test/scala/io/scalaland/chimney/example/internal/MyTypeClassDerivation.scala b/chimney-engine/src/test/scala/io/scalaland/chimney/example/internal/MyTypeClassDerivation.scala new file mode 100644 index 000000000..5d3e3ff64 --- /dev/null +++ b/chimney-engine/src/test/scala/io/scalaland/chimney/example/internal/MyTypeClassDerivation.scala @@ -0,0 +1,72 @@ +package io.scalaland.chimney.example.internal + +import io.scalaland.chimney.example.MyTypeClass +import io.scalaland.chimney.internal.compiletime.DerivationEngine + +trait MyTypeClassDerivation extends DerivationEngine { + + // example of platform-independent type definition + protected val MyTypes: MyTypesModule + protected trait MyTypesModule { this: MyTypes.type => + + // Provides + // - MyTypeClass.apply[From, To]: Type[MyTypeClass[From, To]] + // - MyTypeClass.unapply(tpe: Type[Any]): Option[(??, ??)] // existential types + val MyTypeClass: MyTypeClassModule + trait MyTypeClassModule extends Type.Ctor2[MyTypeClass] { this: MyTypeClass.type => } + + // use in platform-independent code (it cannot generate Type instances, as opposed to Scala 2/Scala 3 macros) + object Implicits { + + implicit def MyTypeClassType[From: Type, To: Type]: Type[MyTypeClass[From, To]] = MyTypeClass[From, To] + } + } + + // example of platform-independent expr utility + protected val MyExprs: MyExprsModule + protected trait MyExprsModule { this: MyExprs.type => + + import MyTypes.Implicits.* + + def callMyTypeClass[From: Type, To: Type](tc: Expr[MyTypeClass[From, To]], from: Expr[From]): Expr[To] + + def createTypeClass[From: Type, To: Type](body: Expr[From] => Expr[To]): Expr[MyTypeClass[From, To]] + + def summonMyTypeClass[From: Type, To: Type]: Option[Expr[MyTypeClass[From, To]]] = + Expr.summonImplicit[MyTypeClass[From, To]] + + // use in platform-independent code (since it does not have quotes nor quasiquotes) + object Implicits { + + implicit class MyTypeClassOps[From: Type, To: Type](private val tc: Expr[MyTypeClass[From, To]]) { + + def convert(from: Expr[From]): Expr[To] = callMyTypeClass(tc, from) + } + } + } + + import MyExprs.Implicits.* + + // example of a platform-independent Rule + object MyTypeClassImplicitRule extends Rule("MyTypeClassImplicit") { + + override def expand[From, To](implicit + ctx: TransformationContext[From, To] + ): DerivationResult[Rule.ExpansionResult[To]] = + MyExprs.summonMyTypeClass[From, To] match { + case Some(myTypeClass) => DerivationResult.expandedTotal(myTypeClass.convert(ctx.src)) + case None => DerivationResult.attemptNextRule + } + } + + def myTypeClassDerivation[From: Type, To: Type]: Expr[MyTypeClass[From, To]] = + MyExprs.createTypeClass[From, To] { (from: Expr[From]) => + val cfg = TransformerConfiguration() // customize, read config with DSL etc + val context = TransformationContext.ForTotal.create[From, To](from, cfg) + + deriveFinalTransformationResultExpr(context).toEither.fold( + derivationErrors => reportError(derivationErrors.toString), // customize + identity + ) + } +}