From d4161e3855f8af752502ff074ad8cad454e1e389 Mon Sep 17 00:00:00 2001 From: Ken Scambler Date: Thu, 18 Feb 2021 00:09:33 +1100 Subject: [PATCH 1/3] Each feature --- .../src/main/scala-3.x/monocle/Focus.scala | 1 + .../monocle/internal/focus/FocusBase.scala | 3 +- .../internal/focus/GeneratorLoop.scala | 60 +++---------------- .../monocle/internal/focus/ParserLoop.scala | 13 ++-- .../features/castas/CastAsGenerator.scala | 5 +- .../focus/features/each/EachGenerator.scala | 16 +++++ .../focus/features/each/EachParser.scala | 22 +++++++ .../fieldselect/FieldSelectGenerator.scala | 2 +- .../optionsome/OptionSomeGenerator.scala | 3 +- .../monocle/syntax/FocusSyntax.scala | 7 ++- .../monocle/focus/FocusEachTest.scala | 48 +++++++++++++++ 11 files changed, 118 insertions(+), 62 deletions(-) create mode 100644 core/shared/src/main/scala-3.x/monocle/internal/focus/features/each/EachGenerator.scala create mode 100644 core/shared/src/main/scala-3.x/monocle/internal/focus/features/each/EachParser.scala create mode 100644 core/shared/src/test/scala-3.x/monocle/focus/FocusEachTest.scala diff --git a/core/shared/src/main/scala-3.x/monocle/Focus.scala b/core/shared/src/main/scala-3.x/monocle/Focus.scala index f919c83d6..eb612643d 100644 --- a/core/shared/src/main/scala-3.x/monocle/Focus.scala +++ b/core/shared/src/main/scala-3.x/monocle/Focus.scala @@ -1,5 +1,6 @@ package monocle +import monocle.function.Each import monocle.internal.focus.{FocusImpl, AppliedFocusImpl} import monocle.syntax.FocusSyntax diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/FocusBase.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/FocusBase.scala index fccc0a1ca..357d630e2 100644 --- a/core/shared/src/main/scala-3.x/monocle/internal/focus/FocusBase.scala +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/FocusBase.scala @@ -14,11 +14,13 @@ private[focus] trait FocusBase { case FieldSelect(name: String, fromType: TypeRepr, fromTypeArgs: List[TypeRepr], toType: TypeRepr) case OptionSome(toType: TypeRepr) case CastAs(fromType: TypeRepr, toType: TypeRepr) + case Each(fromType: TypeRepr, toType: TypeRepr, eachInstance: Term) override def toString(): String = this match { case FieldSelect(name, fromType, fromTypeArgs, toType) => s"FieldSelect($name, ${fromType.show}, ${fromTypeArgs.map(_.show)}, ${toType.show})" case OptionSome(toType) => s"OptionSome(${toType.show})" case CastAs(fromType, toType) => s"CastAs(${fromType.show}, ${toType.show})" + case Each(fromType, toType, eachInstance) => s"Each(${fromType.show}, ${toType.show}, ...)" } } @@ -40,5 +42,4 @@ private[focus] trait FocusBase { } type FocusResult[+A] = Either[FocusError, A] - type ParseResult = FocusResult[List[FocusAction]] } \ No newline at end of file diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/GeneratorLoop.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/GeneratorLoop.scala index 5aa05c45d..3e25973d2 100644 --- a/core/shared/src/main/scala-3.x/monocle/internal/focus/GeneratorLoop.scala +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/GeneratorLoop.scala @@ -3,7 +3,8 @@ package monocle.internal.focus import monocle.internal.focus.features.fieldselect.FieldSelectGenerator import monocle.internal.focus.features.optionsome.OptionSomeGenerator import monocle.internal.focus.features.castas.CastAsGenerator -import monocle.{Lens, Iso, Prism, Optional} +import monocle.internal.focus.features.each.EachGenerator +import monocle.{Lens, Iso, Prism, Optional, Traversal} import scala.quoted.Type @@ -12,6 +13,7 @@ private[focus] trait AllGenerators with FieldSelectGenerator with OptionSomeGenerator with CastAsGenerator + with EachGenerator private[focus] trait GeneratorLoop { this: FocusBase with AllGenerators => @@ -31,60 +33,14 @@ private[focus] trait GeneratorLoop { case FocusAction.FieldSelect(name, fromType, fromTypeArgs, toType) => generateFieldSelect(name, fromType, fromTypeArgs, toType) case FocusAction.OptionSome(toType) => generateOptionSome(toType) case FocusAction.CastAs(fromType, toType) => generateCastAs(fromType, toType) + case FocusAction.Each(fromType, toType, eachInstance) => generateEach(fromType, toType, eachInstance) } private def composeOptics(lens1: Term, lens2: Term): FocusResult[Term] = { - (lens1.tpe.asType, lens2.tpe.asType) match { - case ('[Lens[from1, to1]], '[Lens[from2, to2]]) => - Right('{ ${lens1.asExprOf[Lens[from1, to1]]}.andThen(${lens2.asExprOf[Lens[to1, to2]]}) }.asTerm) - - case ('[Lens[from1, to1]], '[Prism[from2, to2]]) => - Right('{ ${lens1.asExprOf[Lens[from1, to1]]}.andThen(${lens2.asExprOf[Prism[to1, to2]]}) }.asTerm) - - case ('[Lens[from1, to1]], '[Optional[from2, to2]]) => - Right('{ ${lens1.asExprOf[Lens[from1, to1]]}.andThen(${lens2.asExprOf[Optional[to1, to2]]}) }.asTerm) - - case ('[Lens[from1, to1]], '[Iso[from2, to2]]) => - Right('{ ${lens1.asExprOf[Lens[from1, to1]]}.andThen(${lens2.asExprOf[Iso[to1, to2]]}) }.asTerm) - - case ('[Prism[from1, to1]], '[Prism[from2, to2]]) => - Right('{ ${lens1.asExprOf[Prism[from1, to1]]}.andThen(${lens2.asExprOf[Prism[to1, to2]]}) }.asTerm) - - case ('[Prism[from1, to1]], '[Lens[from2, to2]]) => - Right('{ ${lens1.asExprOf[Prism[from1, to1]]}.andThen(${lens2.asExprOf[Lens[to1, to2]]}) }.asTerm) - - case ('[Prism[from1, to1]], '[Optional[from2, to2]]) => - Right('{ ${lens1.asExprOf[Prism[from1, to1]]}.andThen(${lens2.asExprOf[Optional[to1, to2]]}) }.asTerm) - - case ('[Prism[from1, to1]], '[Iso[from2, to2]]) => - Right('{ ${lens1.asExprOf[Prism[from1, to1]]}.andThen(${lens2.asExprOf[Iso[to1, to2]]}) }.asTerm) - - case ('[Optional[from1, to1]], '[Lens[from2, to2]]) => - Right('{ ${lens1.asExprOf[Optional[from1, to1]]}.andThen(${lens2.asExprOf[Lens[to1, to2]]}) }.asTerm) - - case ('[Optional[from1, to1]], '[Optional[from2, to2]]) => - Right('{ ${lens1.asExprOf[Optional[from1, to1]]}.andThen(${lens2.asExprOf[Optional[to1, to2]]}) }.asTerm) - - case ('[Optional[from1, to1]], '[Prism[from2, to2]]) => - Right('{ ${lens1.asExprOf[Optional[from1, to1]]}.andThen(${lens2.asExprOf[Prism[to1, to2]]}) }.asTerm) - - case ('[Optional[from1, to1]], '[Iso[from2, to2]]) => - Right('{ ${lens1.asExprOf[Optional[from1, to1]]}.andThen(${lens2.asExprOf[Iso[to1, to2]]}) }.asTerm) - - case ('[Iso[from1, to1]], '[Lens[from2, to2]]) => - Right('{ ${lens1.asExprOf[Iso[from1, to1]]}.andThen(${lens2.asExprOf[Lens[to1, to2]]}) }.asTerm) - - case ('[Iso[from1, to1]], '[Iso[from2, to2]]) => - Right('{ ${lens1.asExprOf[Iso[from1, to1]]}.andThen(${lens2.asExprOf[Iso[to1, to2]]}) }.asTerm) - - case ('[Iso[from1, to1]], '[Optional[from2, to2]]) => - Right('{ ${lens1.asExprOf[Iso[from1, to1]]}.andThen(${lens2.asExprOf[Optional[to1, to2]]}) }.asTerm) - - case ('[Iso[from1, to1]], '[Prism[from2, to2]]) => - Right('{ ${lens1.asExprOf[Iso[from1, to1]]}.andThen(${lens2.asExprOf[Prism[to1, to2]]}) }.asTerm) - - case ('[a], '[b]) => - FocusError.ComposeMismatch(TypeRepr.of[a].show, TypeRepr.of[b].show).asResult + lens2.tpe match { + // Won't yet work for polymorphism where A != B, nor for non-polymorphic optics Getter, Setter or Fold. + case AppliedType(_, List(_, toType2)) => Right(Select.overloaded(lens1, "andThen", List(toType2, toType2), List(lens2))) + case _ => FocusError.ComposeMismatch(lens1.tpe.show, lens2.tpe.show).asResult } } } \ No newline at end of file diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/ParserLoop.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/ParserLoop.scala index 3798d1348..900ec68b1 100644 --- a/core/shared/src/main/scala-3.x/monocle/internal/focus/ParserLoop.scala +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/ParserLoop.scala @@ -4,19 +4,21 @@ import scala.quoted.Type import monocle.internal.focus.features.fieldselect.FieldSelectParser import monocle.internal.focus.features.optionsome.OptionSomeParser import monocle.internal.focus.features.castas.CastAsParser +import monocle.internal.focus.features.each.EachParser private[focus] trait AllParsers extends FocusBase with FieldSelectParser with OptionSomeParser with CastAsParser + with EachParser private[focus] trait ParserLoop { this: FocusBase with AllParsers => import macroContext.reflect._ - def parseLambda[From: Type](lambda: Term): ParseResult = { + def parseLambda[From: Type](lambda: Term): FocusResult[List[FocusAction]] = { val fromTypeIsConcrete = TypeRepr.of[From].classSymbol.isDefined lambda match { @@ -35,8 +37,8 @@ private[focus] trait ParserLoop { } } - private def parseLambdaBody(params: ParseParams): ParseResult = { - def loop(remainingBody: Term, listSoFar: List[FocusAction]): ParseResult = { + private def parseLambdaBody(params: ParseParams): FocusResult[List[FocusAction]] = { + def loop(remainingBody: Term, listSoFar: List[FocusAction]): FocusResult[List[FocusAction]] = { remainingBody match { case LambdaArgument(idName) if idName == params.argName => Right(listSoFar) @@ -45,6 +47,9 @@ private[focus] trait ParserLoop { case OptionSome(Right(remainingCode, action)) => loop(remainingCode, action :: listSoFar) case OptionSome(Left(error)) => Left(error) + case Each(Right(remainingCode, action)) => loop(remainingCode, action :: listSoFar) + case Each(Left(error)) => Left(error) + case CastAs(Right(remainingCode, action)) => loop(remainingCode, action :: listSoFar) case CastAs(Left(error)) => Left(error) @@ -56,7 +61,7 @@ private[focus] trait ParserLoop { } loop(params.lambdaBody, Nil) } - + private def unwrap(term: Term): Term = { term match { case Block(List(), inner) => unwrap(inner) diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/features/castas/CastAsGenerator.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/castas/CastAsGenerator.scala index c0bc16cf9..3881457d4 100644 --- a/core/shared/src/main/scala-3.x/monocle/internal/focus/features/castas/CastAsGenerator.scala +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/castas/CastAsGenerator.scala @@ -1,5 +1,6 @@ package monocle.internal.focus.features.castas +import monocle.Prism import monocle.internal.focus.FocusBase private[focus] trait CastAsGenerator { @@ -10,8 +11,8 @@ private[focus] trait CastAsGenerator { def generateCastAs(fromType: TypeRepr, toType: TypeRepr): Term = { (fromType.asType, toType.asType) match { case ('[f], '[t]) => '{ - _root_.monocle.Prism[f, t]((from: f) => if (from.isInstanceOf[t]) Some(from.asInstanceOf[t]) else None) - ((to: t) => to.asInstanceOf[f]) }.asTerm + Prism[f, t]((from: f) => if (from.isInstanceOf[t]) Some(from.asInstanceOf[t]) else None) + ((to: t) => to.asInstanceOf[f]) }.asTerm } } } \ No newline at end of file diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/features/each/EachGenerator.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/each/EachGenerator.scala new file mode 100644 index 000000000..bca77b01c --- /dev/null +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/each/EachGenerator.scala @@ -0,0 +1,16 @@ +package monocle.internal.focus.features.each + +import monocle.function.Each +import monocle.internal.focus.FocusBase + +private[focus] trait EachGenerator { + this: FocusBase => + + import macroContext.reflect._ + + def generateEach(fromType: TypeRepr, toType: TypeRepr, eachInstance: Term): Term = + (fromType.asType, toType.asType) match { + case ('[f], '[t]) => '{(${eachInstance.asExprOf[Each[f, t]]}.each)}.asTerm + } + +} \ No newline at end of file diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/features/each/EachParser.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/each/EachParser.scala new file mode 100644 index 000000000..26533c238 --- /dev/null +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/each/EachParser.scala @@ -0,0 +1,22 @@ +package monocle.internal.focus.features.each + +import monocle.internal.focus.FocusBase + +private[focus] trait EachParser { + this: FocusBase => + + import macroContext.reflect._ + + object Each extends FocusParser { + + def unapply(term: Term): Option[FocusResult[(Term, FocusAction)]] = term match { + case Apply(Apply(TypeApply(Ident("each"), List(_, toTypeTree)), List(remainingCode)), List(eachInstance)) => + val fromType = remainingCode.tpe.widen + val toType = toTypeTree.tpe + val action = FocusAction.Each(fromType, toType, eachInstance) + Some(Right(remainingCode, action)) + + case _ => None + } + } +} \ No newline at end of file diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/features/fieldselect/FieldSelectGenerator.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/fieldselect/FieldSelectGenerator.scala index c7bb589e3..16a51c2a8 100644 --- a/core/shared/src/main/scala-3.x/monocle/internal/focus/features/fieldselect/FieldSelectGenerator.scala +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/fieldselect/FieldSelectGenerator.scala @@ -19,7 +19,7 @@ private[focus] trait FieldSelectGenerator { val getter: f => t = (from: f) => ${ generateGetter(field, '{from}.asTerm).asExprOf[t] } - _root_.monocle.Lens.apply[f, t](getter)(setter) + Lens.apply[f, t](getter)(setter) }.asTerm } } diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/features/optionsome/OptionSomeGenerator.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/optionsome/OptionSomeGenerator.scala index 8d19c5b50..ac3e93c54 100644 --- a/core/shared/src/main/scala-3.x/monocle/internal/focus/features/optionsome/OptionSomeGenerator.scala +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/optionsome/OptionSomeGenerator.scala @@ -1,6 +1,7 @@ package monocle.internal.focus.features.optionsome import monocle.internal.focus.FocusBase +import monocle.std.option.some private[focus] trait OptionSomeGenerator { this: FocusBase => @@ -9,7 +10,7 @@ private[focus] trait OptionSomeGenerator { def generateOptionSome(toType: TypeRepr): Term = { toType.asType match { - case '[t] => '{ _root_.monocle.std.option.some[t] }.asTerm + case '[t] => '{ some[t] }.asTerm } } } \ No newline at end of file diff --git a/core/shared/src/main/scala-3.x/monocle/syntax/FocusSyntax.scala b/core/shared/src/main/scala-3.x/monocle/syntax/FocusSyntax.scala index 7d8dbef84..22917d8b6 100644 --- a/core/shared/src/main/scala-3.x/monocle/syntax/FocusSyntax.scala +++ b/core/shared/src/main/scala-3.x/monocle/syntax/FocusSyntax.scala @@ -1,9 +1,14 @@ package monocle.syntax +import monocle.function.Each + trait FocusSyntax { extension [CastTo] (from: Any) def as: CastTo = scala.sys.error("Extension method 'as[CastTo]' should only be used within the monocle.Focus macro.") extension [A] (opt: Option[A]) - def some: A = scala.sys.error("Extension method 'some' should only be used within the monocle.Focus macro.") + def some: A = scala.sys.error("Extension method 'some' should only be used within the monocle.Focus macro.") + + extension [From, To] (from: From)(using Each[From, To]) + def each: To = scala.sys.error("Extension method 'each' should only be used within the monocle.Focus macro.") } diff --git a/core/shared/src/test/scala-3.x/monocle/focus/FocusEachTest.scala b/core/shared/src/test/scala-3.x/monocle/focus/FocusEachTest.scala new file mode 100644 index 000000000..ad4ff16e9 --- /dev/null +++ b/core/shared/src/test/scala-3.x/monocle/focus/FocusEachTest.scala @@ -0,0 +1,48 @@ +package monocle.focus + +import monocle.Focus +import monocle.Focus._ +import monocle.function.Each._ +import monocle.std.list._ + +final class FocusEachTest extends munit.FunSuite { + + test("Direct each on the argument") { + val eachNumber = Focus[List[Int]](_.each) + val list = List(1,2,3) + assertEquals(eachNumber.getAll(list), List(1,2,3)) + assertEquals(eachNumber.modify(_ + 1)(list), List(2,3,4)) + } + + test("Each on a field") { + case class School(name: String, students: List[Student]) + case class Student(firstName: String, lastName: String, yearLevel: Int) + + + val school = School("Sparkvale Primary School", List( + Student("Arlen", "Appleby", 5), + Student("Bob", "Bobson", 6), + Student("Carol", "Cornell", 7) + )) + + val studentNames = Focus[School](_.students.each.firstName) + val studentYears = Focus[School](_.students.each.yearLevel) + + assertEquals(studentNames.getAll(school), List("Arlen", "Bob", "Carol")) + } + + test("Focus operator each commutes with standalone operator each") { + case class School(name: String, students: List[Student]) + case class Student(firstName: String, lastName: String, yearLevel: Int) + + val school = School("Sparkvale Primary School", List( + Student("Arlen", "Appleby", 5), + Student("Bob", "Bobson", 6), + Student("Carol", "Cornell", 7) + )) + + assertEquals( + Focus[School](_.students.each).getAll(school), + Focus[School](_.students).each.getAll(school)) + } +} From d77d8c42a9cdfc40bd846fe4543c8cb4c71904ef Mon Sep 17 00:00:00 2001 From: Ken Scambler Date: Thu, 18 Feb 2021 00:17:09 +1100 Subject: [PATCH 2/3] Fixed tiny indentation thing --- .../focus/features/fieldselect/FieldSelectGenerator.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/features/fieldselect/FieldSelectGenerator.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/fieldselect/FieldSelectGenerator.scala index 16a51c2a8..765d3824a 100644 --- a/core/shared/src/main/scala-3.x/monocle/internal/focus/features/fieldselect/FieldSelectGenerator.scala +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/features/fieldselect/FieldSelectGenerator.scala @@ -19,7 +19,7 @@ private[focus] trait FieldSelectGenerator { val getter: f => t = (from: f) => ${ generateGetter(field, '{from}.asTerm).asExprOf[t] } - Lens.apply[f, t](getter)(setter) + Lens.apply[f, t](getter)(setter) }.asTerm } } From f55ea27e6720d0f0a070a41f7e5031333f9b5934 Mon Sep 17 00:00:00 2001 From: Ken Scambler Date: Thu, 18 Feb 2021 00:29:07 +1100 Subject: [PATCH 3/3] Fixed broken test, composed optic types needed widening, so as not to get stuck on singleton paths --- .../main/scala-3.x/monocle/internal/focus/GeneratorLoop.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/GeneratorLoop.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/GeneratorLoop.scala index 3e25973d2..342d11d51 100644 --- a/core/shared/src/main/scala-3.x/monocle/internal/focus/GeneratorLoop.scala +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/GeneratorLoop.scala @@ -37,7 +37,7 @@ private[focus] trait GeneratorLoop { } private def composeOptics(lens1: Term, lens2: Term): FocusResult[Term] = { - lens2.tpe match { + lens2.tpe.widen match { // Won't yet work for polymorphism where A != B, nor for non-polymorphic optics Getter, Setter or Fold. case AppliedType(_, List(_, toType2)) => Right(Select.overloaded(lens1, "andThen", List(toType2, toType2), List(lens2))) case _ => FocusError.ComposeMismatch(lens1.tpe.show, lens2.tpe.show).asResult