From 4190cc0d64b2e1fb745a16840d14b7038f39bed7 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Sun, 17 Mar 2024 00:06:12 +0100 Subject: [PATCH] Companion object-based Contains derivation (fix #143) --- .gitignore | 1 + build.sbt | 11 ++- .../{scala => scala-2}/glass/package.scala | 2 - .../core/src/main/scala-3/glass/glass.scala | 27 ++++++ .../scala-3/glass/macros/DeriveContains.scala | 10 +++ .../scala-3/glass/macros/GenContains.scala | 29 +----- .../macros/internal/ContainsSelector.scala | 7 ++ .../scala-3/glass/macros/internal/impl.scala | 89 +++++++++++++++++++ .../scala-3/glass/macros/internal/utils.scala | 53 ----------- .../glass/macros/GenContainsSpec.scala | 8 +- .../scala-3/glass/macros/TestDomain.scala | 1 + 11 files changed, 148 insertions(+), 90 deletions(-) rename modules/core/src/main/{scala => scala-2}/glass/package.scala (93%) create mode 100644 modules/core/src/main/scala-3/glass/glass.scala create mode 100644 modules/macro/src/main/scala-3/glass/macros/DeriveContains.scala create mode 100644 modules/macro/src/main/scala-3/glass/macros/internal/ContainsSelector.scala create mode 100644 modules/macro/src/main/scala-3/glass/macros/internal/impl.scala delete mode 100644 modules/macro/src/main/scala-3/glass/macros/internal/utils.scala diff --git a/.gitignore b/.gitignore index 4753948..3396317 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ node_modules/ .bsp *.db .jvmopts +**/.DS_Store \ No newline at end of file diff --git a/build.sbt b/build.sbt index f901026..bb9642a 100644 --- a/build.sbt +++ b/build.sbt @@ -77,7 +77,16 @@ lazy val opticsMacro = project ) opts.filterNot(opt => suppressed.exists(opt.contains)) }, - name := "glass-macro" + name := "glass-macro", + libraryDependencies ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _)) => + Seq( + "io.github.kitlangton" %% "quotidian" % "0.0.14" + ) + case _ => Seq.empty + } + } ) lazy val coreModules = diff --git a/modules/core/src/main/scala/glass/package.scala b/modules/core/src/main/scala-2/glass/package.scala similarity index 93% rename from modules/core/src/main/scala/glass/package.scala rename to modules/core/src/main/scala-2/glass/package.scala index f080cf9..f6d1ae2 100644 --- a/modules/core/src/main/scala/glass/package.scala +++ b/modules/core/src/main/scala-2/glass/package.scala @@ -1,8 +1,6 @@ /** a collection of classic monomorphic optics based on http://hackage.haskell.org/package/lens and * http://julien-truffaut.github.io/Monocle/ using names readable for user unfamiliar with then capable for using as * implicit evidences in effect transmogrification - * - * see hierarchy here: https://wiki.tcsbank.ru/display/API/optics */ package object glass { type Same[A, B] = PSame[A, A, B, B] diff --git a/modules/core/src/main/scala-3/glass/glass.scala b/modules/core/src/main/scala-3/glass/glass.scala new file mode 100644 index 0000000..4e489a5 --- /dev/null +++ b/modules/core/src/main/scala-3/glass/glass.scala @@ -0,0 +1,27 @@ +package glass + +/** a collection of classic monomorphic optics based on http://hackage.haskell.org/package/lens and + * http://julien-truffaut.github.io/Monocle/ using names readable for user unfamiliar with then capable for using as + * implicit evidences in effect transmogrification + */ + +type Same[A, B] = PSame[A, A, B, B] +type Equivalent[A, B] = PEquivalent[A, A, B, B] +type Subset[A, B] = PSubset[A, A, B, B] +type Contains[A, B] = PContains[A, A, B, B] +type Property[A, B] = PProperty[A, A, B, B] +type Repeated[A, B] = PRepeated[A, A, B, B] +type Items[A, B] = PItems[A, A, B, B] +type Reduced[A, B] = PReduced[A, A, B, B] +type Downcast[A, B] = PDowncast[A, A, B, B] +type Upcast[A, B] = PUpcast[A, A, B, B] +type Extract[A, B] = PExtract[A, A, B, B] +type Folded[A, B] = PFolded[A, A, B, B] +type Update[A, B] = PUpdate[A, A, B, B] +type Zipping[A, B] = PZipping[A, A, B, B] + +/** label provider for instance discrimination like Contains[A, B] with Label["first"] + */ +type Label[label] = Any { + type Label = label +} diff --git a/modules/macro/src/main/scala-3/glass/macros/DeriveContains.scala b/modules/macro/src/main/scala-3/glass/macros/DeriveContains.scala new file mode 100644 index 0000000..97362be --- /dev/null +++ b/modules/macro/src/main/scala-3/glass/macros/DeriveContains.scala @@ -0,0 +1,10 @@ +package glass.macros + +import glass.macros.internal.{CompanionClass, ContainsFor} + +trait DeriveContains: + given conversion(using + cc: CompanionClass[this.type], + contains: ContainsFor[cc.Type] + ): Conversion[this.type, contains.Out] = + _ => contains.contains diff --git a/modules/macro/src/main/scala-3/glass/macros/GenContains.scala b/modules/macro/src/main/scala-3/glass/macros/GenContains.scala index 41ef389..e2e1856 100644 --- a/modules/macro/src/main/scala-3/glass/macros/GenContains.scala +++ b/modules/macro/src/main/scala-3/glass/macros/GenContains.scala @@ -9,31 +9,4 @@ object GenContains: def apply[A]: MkContains[A] = new MkContains[A] class MkContains[A]: - inline def apply[B](inline expr: A => B): Contains[A, B] = ${ mkContains('expr) } - -def mkContains[A: Type, B: Type](expr: Expr[A => B])(using qctx: Quotes) = - import qctx.reflect.* - - val (_, term) = unwrapLambda(expr.asTerm) - - term match { - case Select(CaseClass(cc), name) => - val ccType = cc.tpe.widen.dealias - val fieldType = - ccType.classSymbol - .map(_.fieldMembers.find(_.name.trim == name).getOrElse(Symbol.noSymbol)) - .flatMap { - case FieldType(ta) => Some(ta) - case _ => None - } - .getOrElse(quotes.reflect.report.errorAndAbort("Can't get field type")) - - (ccType.asType, fieldType.asType) match { - case ('[a], '[b]) => - '{ - Contains[a]((from: a) => ${ Select.unique('{ from }.asTerm, name).asExprOf[b] })((from: a, to: b) => - ${ Select.overloaded('{ from }.asTerm, "copy", Nil, NamedArg(name, '{ to }.asTerm) :: Nil).asExprOf[a] } - ) - }.asExprOf[Contains[A, B]] - } - } + inline def apply[B](inline expr: A => B): Contains[A, B] = ${ ContainsMacro.mkContainsImpl('expr) } diff --git a/modules/macro/src/main/scala-3/glass/macros/internal/ContainsSelector.scala b/modules/macro/src/main/scala-3/glass/macros/internal/ContainsSelector.scala new file mode 100644 index 0000000..8b43ed2 --- /dev/null +++ b/modules/macro/src/main/scala-3/glass/macros/internal/ContainsSelector.scala @@ -0,0 +1,7 @@ +package glass.macros.internal + +import glass.* + +private[macros] class ContainsSelector[A](lenses: Map[String, PContains[A, A, ?, ?]]) extends Selectable: + inline def selectDynamic(name: String): PContains[A, A, ?, ?] = + lenses(name) diff --git a/modules/macro/src/main/scala-3/glass/macros/internal/impl.scala b/modules/macro/src/main/scala-3/glass/macros/internal/impl.scala new file mode 100644 index 0000000..3f8ca4b --- /dev/null +++ b/modules/macro/src/main/scala-3/glass/macros/internal/impl.scala @@ -0,0 +1,89 @@ +package glass.macros.internal + +import glass.* +import quotidian.* +import quotidian.syntax.* +import quoted.* + +private[macros] object ContainsMacro: + def mkContainsImpl[A: Type, B: Type](expr: Expr[A => B])(using Quotes): Expr[PContains[A, A, B, B]] = + import quotes.reflect.* + expr.asTerm.underlyingArgument match + case Lambda(_, select @ Select(a, b)) => + val productMirror = MacroMirror.summonProduct[A] + + val elem = productMirror + .elemForSymbol(select.symbol) + .getOrElse( + report.errorAndAbort(s"Invalid selector ${select.show}, must be a field of ${productMirror.monoType.show}") + ) + .asElemOf[B] + + '{ + new PContains[A, A, B, B]: + def extract(a: A) = ${ elem.get('a) } + def set(a: A, b: B) = ${ elem.set('a, 'b) } + } + case other => report.errorAndAbort(s"Expected a selector of the form `s => a`, but got: ${other}") + + implicit transparent inline def makeContainses[S]: Any = ${ makeContainsesImpl[S] } + + def makeContainsesImpl[S: Type](using Quotes): Expr[Any] = + import quotes.reflect.* + + val productMirror = MacroMirror.summonProduct[S] + + val containsMap = Expr.ofMap(productMirror.elems.map { elem => + import elem.asType + val selector = '{ (s: S) => ${ elem.get('s) } } + elem.label -> mkContainsImpl(selector) + }) + + val refinedType = + Refinement.of[ContainsSelector[S]]( + productMirror.elemsWithTypes.map { case (elem, '[a]) => + elem.label -> TypeRepr.of[PContains[S, S, a, a]] + } + ) + + refinedType.asType match + case '[t] => + '{ new ContainsSelector[S]($containsMap).asInstanceOf[t] } + +private[macros] trait ContainsFor[A]: + type Out + + def contains: Out + +private[macros] object ContainsFor: + transparent inline given derived[A]: ContainsFor[A] = ${ containsForImpl[A] } + + private def containsForImpl[A: Type](using Quotes) = + import quotes.reflect.* + val lensesExpr = ContainsMacro.makeContainsesImpl[A] + lensesExpr.asTerm.tpe.asType match + case '[t] => + '{ + new ContainsFor[A]: + type Out = t + + def contains: t = $lensesExpr.asInstanceOf[t] + } + +private[macros] trait CompanionClass[A]: + type Type + +private[macros] object CompanionClass: + transparent inline given [A]: CompanionClass[A] = ${ companionImpl[A] } + + private def companionImpl[A: Type](using Quotes) = + import quotes.reflect.* + val companionClass = TypeRepr.companionClassOf[A] + if companionClass.typeSymbol.isNoSymbol then report.errorAndAbort(s"No companion class found for ${Type.show[A]}") + + TypeRepr.companionClassOf[A].asType match + case '[t] => + '{ + new CompanionClass[A]: + type Type = t + } diff --git a/modules/macro/src/main/scala-3/glass/macros/internal/utils.scala b/modules/macro/src/main/scala-3/glass/macros/internal/utils.scala deleted file mode 100644 index 8e200d1..0000000 --- a/modules/macro/src/main/scala-3/glass/macros/internal/utils.scala +++ /dev/null @@ -1,53 +0,0 @@ -package glass.macros.internal - -import scala.quoted.* - -object AsTerm: - def unapply(using quotes: Quotes)(expr: Expr[Any]): Option[quotes.reflect.Term] = { - import quotes.reflect.* - Some(expr.asTerm) - } - -object AnonfunBlock: - def unapply(using quotes: Quotes)(term: quotes.reflect.Term): Option[(String, quotes.reflect.Term)] = { - import quotes.reflect.* - term match { - case Lambda(ValDef(paramName, _, _) :: Nil, rhs) => Some((paramName, rhs)) - case _ => None - } - } - -object CaseClass: - def unapply(using quotes: Quotes)(term: quotes.reflect.Term): Option[quotes.reflect.Term] = - term.tpe.classSymbol.flatMap { sym => - Option.when(sym.flags.is(quotes.reflect.Flags.Case))(term) - } - -object FieldType: - def unapply(using quotes: Quotes)(fieldSymbol: quotes.reflect.Symbol): Option[quotes.reflect.TypeRepr] = - import quotes.reflect.* - - fieldSymbol match { - case sym if sym.isNoSymbol => None - case sym => - sym.tree match { - case ValDef(_, typeTree, _) => Some(typeTree.tpe) - case _ => None - } - } - -def showTerm(using quotes: Quotes)(term: quotes.reflect.Term) = - term.show(using quotes.reflect.Printer.TreeStructure) - -def unwrapLambda(using quotes: Quotes)(input: quotes.reflect.Term): (String, quotes.reflect.Term) = - input match { - case quotes.reflect.Inlined(_, _, expansion) => unwrapLambda(expansion) - case AnonfunBlock(paramName, body) => (paramName, body) - case _ => quotes.reflect.report.errorAndAbort(s"Expected a lambda, got ${showTerm(input)}") - } - -def getSuppliedTypeArgs(using quotes: Quotes)(fromType: quotes.reflect.TypeRepr): List[quotes.reflect.TypeRepr] = - fromType match { - case quotes.reflect.AppliedType(_, argTypeReprs) => argTypeReprs - case _ => Nil - } diff --git a/modules/macro/src/test/scala-3/glass/macros/GenContainsSpec.scala b/modules/macro/src/test/scala-3/glass/macros/GenContainsSpec.scala index 14770ef..f28b356 100644 --- a/modules/macro/src/test/scala-3/glass/macros/GenContainsSpec.scala +++ b/modules/macro/src/test/scala-3/glass/macros/GenContainsSpec.scala @@ -5,21 +5,17 @@ import org.scalatest.matchers.should.Matchers.shouldBe class GenContainsSpec extends AnyFunSuite: test("Get") { - val lens = GenContains[Bar](_.i) - val data = Bar(42) - val sut = lens.get(data) + val sut = Bar.i.get(data) sut shouldBe 42 } test("Set") { - val lens = GenContains[Bar](_.i) - val data = Bar(42) - val sut = lens.update(data, _ + 1) + val sut = Bar.i.update(data, _ + 1) sut.i shouldBe 43 } diff --git a/modules/macro/src/test/scala-3/glass/macros/TestDomain.scala b/modules/macro/src/test/scala-3/glass/macros/TestDomain.scala index acdc194..3cd30a1 100644 --- a/modules/macro/src/test/scala-3/glass/macros/TestDomain.scala +++ b/modules/macro/src/test/scala-3/glass/macros/TestDomain.scala @@ -5,3 +5,4 @@ case object B extends A case object C extends A case class Bar(i: Int) +object Bar extends DeriveContains