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 a94361328..8327b7692 100644 --- a/core/shared/src/main/scala-3.x/monocle/Focus.scala +++ b/core/shared/src/main/scala-3.x/monocle/Focus.scala @@ -1,10 +1,10 @@ package monocle -import monocle.syntax.AppliedFocusSyntax +import monocle.syntax.{AppliedFocusSyntax, ComposedFocusSyntax} import monocle.internal.focus.FocusImpl import monocle.function.{Each, At, Index} -object Focus extends AppliedFocusSyntax { +object Focus extends AppliedFocusSyntax with ComposedFocusSyntax { sealed trait KeywordContext { extension [From] (from: From) diff --git a/core/shared/src/main/scala-3.x/monocle/internal/focus/ComposedFocusImpl.scala b/core/shared/src/main/scala-3.x/monocle/internal/focus/ComposedFocusImpl.scala new file mode 100644 index 000000000..fb55f6c56 --- /dev/null +++ b/core/shared/src/main/scala-3.x/monocle/internal/focus/ComposedFocusImpl.scala @@ -0,0 +1,26 @@ +package monocle.internal.focus + +import monocle.{Focus, Lens, Iso, Prism, Optional, Traversal, Getter, Setter, Fold, AppliedSetter, AppliedFold, AppliedGetter} +import scala.quoted.{Type, Expr, Quotes, quotes} + +private[monocle] object ComposedFocusImpl { + + type AnyOptic[S,A] = Setter[S,A] | Fold[S,A] | AppliedSetter[S,A] | AppliedFold[S,A] + + def apply[S: Type, A: Type, Next: Type](optic: Expr[AnyOptic[S,A]], lambda: Expr[Focus.KeywordContext ?=> A => Next])(using Quotes): Expr[Any] = { + import quotes.reflect._ + + val generatedOptic = FocusImpl(lambda).asTerm + val opticType = optic.asTerm.tpe.widen + val nextType = TypeRepr.of[Next] + val singleTypeParam: Boolean = + opticType =:= TypeRepr.of[Fold[S,A]] || + opticType =:= TypeRepr.of[Getter[S,A]] || + opticType =:= TypeRepr.of[AppliedFold[S,A]] || + opticType =:= TypeRepr.of[AppliedGetter[S,A]] + + val typeParams = if (singleTypeParam) List(nextType) else List(nextType, nextType) + + Select.overloaded(optic.asTerm, "andThen", typeParams, List(generatedOptic)).asExpr + } +} \ No newline at end of file diff --git a/core/shared/src/main/scala-3.x/monocle/syntax/All.scala b/core/shared/src/main/scala-3.x/monocle/syntax/All.scala index 9c7c7a5d9..8e421ad2f 100644 --- a/core/shared/src/main/scala-3.x/monocle/syntax/All.scala +++ b/core/shared/src/main/scala-3.x/monocle/syntax/All.scala @@ -2,4 +2,4 @@ package monocle.syntax object all extends Syntaxes -trait Syntaxes extends AppliedSyntax with AppliedFocusSyntax with MacroSyntax with FieldsSyntax +trait Syntaxes extends AppliedSyntax with AppliedFocusSyntax with ComposedFocusSyntax with MacroSyntax with FieldsSyntax \ No newline at end of file diff --git a/core/shared/src/main/scala-3.x/monocle/syntax/ComposedFocusSyntax.scala b/core/shared/src/main/scala-3.x/monocle/syntax/ComposedFocusSyntax.scala new file mode 100644 index 000000000..a367bdc18 --- /dev/null +++ b/core/shared/src/main/scala-3.x/monocle/syntax/ComposedFocusSyntax.scala @@ -0,0 +1,12 @@ +package monocle.syntax + +import monocle._ +import monocle.internal.focus.ComposedFocusImpl + +trait ComposedFocusSyntax { + + extension [S, A, Next] (optic: Setter[S, A] | Fold[S,A] | AppliedSetter[S,A] | AppliedFold[S,A]) { + transparent inline def refocus(inline lambda: (Focus.KeywordContext ?=> A => Next)): Any = + ${ComposedFocusImpl[S, A, Next]('optic, 'lambda)} + } +} \ No newline at end of file diff --git a/core/shared/src/test/scala-3.x/monocle/focus/ComposedFocusTest.scala b/core/shared/src/test/scala-3.x/monocle/focus/ComposedFocusTest.scala new file mode 100644 index 000000000..c7a38f08b --- /dev/null +++ b/core/shared/src/test/scala-3.x/monocle/focus/ComposedFocusTest.scala @@ -0,0 +1,162 @@ +package monocle.focus + +import monocle._ +import monocle.syntax.all._ +import monocle.syntax.{AppliedGetter, AppliedSetter} + + +object ComposedFocusTest { + enum Roof { + case Tiles(numTiles: Int) + case Thatch(color: Color) + case Glass(tint: Option[String]) + } + + case class Color(r: Int, g: Int, b: Int) + + case class Mailbox(address: Address) + case class User(name: String, address: Address) + case class Street(name: String) + case class Potato(count: Int) + case class Address(streetNumber: Int, street: Option[Street], roof: Roof, potatoes: List[Potato]) + + case class MailingList(users: List[User]) + + val elise = User("Elise", Address(12, Some(Street("high street")), Roof.Tiles(999), (1 to 4).toList.map(Potato.apply))) + val mailbox = Mailbox(Address(1, Some(Street("cherrytree lane")), Roof.Thatch(Color(255, 255, 0)), Nil)) +} + + +final class ComposedFocusTest extends munit.FunSuite { + + import ComposedFocusTest._ + + test("Lens refocus correctly composes Lens") { + val addressLens: Lens[User, Address] = Focus[User](_.address) + val newLens: Lens[User, Int] = addressLens.refocus(_.streetNumber) + val newElise = newLens.replace(50)(elise) + + assertEquals(newElise.address.streetNumber, 50) + } + + test("AppliedLens refocus correctly composes Lens") { + val addressLens: AppliedLens[User, Address] = elise.focus(_.address) + val newLens: AppliedLens[User, Int] = addressLens.refocus(_.streetNumber) + val newElise = newLens.replace(50) + + assertEquals(newElise.address.streetNumber, 50) + } + + test("Lens refocus correctly composes Prism") { + val roofLens: Lens[User, Roof] = Focus[User](_.address.roof) + val newLens: Optional[User, Roof.Tiles] = roofLens.refocus(_.as[Roof.Tiles]) + val newElise = newLens.replace(Roof.Tiles(3))(elise) + + assertEquals(newElise.address.roof, Roof.Tiles(3)) + } + + test("Lens refocus correctly composes Iso") { + val addressLens: Lens[User, Address] = Focus[User](_.address) + val newLens: Lens[User, Int] = addressLens.refocus(_.streetNumber) + val newElise = newLens.replace(50)(elise) + + assertEquals(newElise.address.streetNumber, 50) + } + + test("Lens refocus correctly composes Optional") { + val addressLens: Lens[User, Address] = Focus[User](_.address) + val newLens: Optional[User, String] = addressLens.refocus(_.street.some.name) + val newElise = newLens.replace("Crunkley Ave")(elise) + + assertEquals(newElise.address.street.map(_.name), Some("Crunkley Ave")) + } + + test("Lens refocus correctly composes Traversal") { + val addressLens: Lens[User, Address] = Focus[User](_.address) + val newLens: Traversal[User, Int] = addressLens.refocus(_.potatoes.each.count) + val newElise = newLens.modify(_ + 1)(elise) + + assertEquals(newElise.address.potatoes.map(_.count), List(2,3,4,5)) + } + + test("Prism refocus correctly composes Lens") { + val oldLens: Prism[Roof, Roof.Thatch] = Focus[Roof](_.as[Roof.Thatch]) + val newLens: Optional[Roof, Int] = oldLens.refocus(_.color.r) + val newRoof = newLens.replace(77)(Roof.Thatch(Color(255, 255, 255))) + + assertEquals(newRoof, Roof.Thatch(Color(77, 255, 255))) + } + + test("Prism refocus correctly composes Prism") { + val oldLens: Prism[Roof, Roof.Glass] = Focus[Roof](_.as[Roof.Glass]) + val newLens: Prism[Roof, String] = oldLens.refocus(_.tint.some) + val newRoof = newLens.replace("light")(Roof.Glass(Some("dark"))) + + assertEquals(newRoof, Roof.Glass(Some("light"))) + } + + test("Prism refocus correctly composes Iso") { + val oldLens: Prism[Roof, Roof.Tiles] = Focus[Roof](_.as[Roof.Tiles]) + val newLens: Prism[Roof, Int] = oldLens.refocus(_.numTiles) + val newRoof = newLens.replace(100)(Roof.Tiles(3)) + + assertEquals(newRoof, Roof.Tiles(100)) + } + + test("Prism refocus correctly composes Optional") { + val oldLens: Prism[Roof, Roof.Glass] = Focus[Roof](_.as[Roof.Glass]) + val newLens: Optional[Roof, String] = oldLens.refocus(_.tint.some) + val newRoof = newLens.replace("light")(Roof.Glass(Some("dark"))) + + assertEquals(newRoof, Roof.Glass(Some("light"))) + } + + test("Fold refocus correctly composes Lens") { + val userFold: Fold[MailingList, Address] = Focus[MailingList](_.users.each).andThen(Getter[User, Address](_.address)) + val newLens: Fold[MailingList, Int] = userFold.refocus(_.streetNumber) + val streetNumbers = newLens.getAll(MailingList(List(elise))) + + assertEquals(streetNumbers, List(12)) + } + + test("AppliedFold refocus correctly composes Lens") { + val mailingList = MailingList(List(elise)) + val userFold: AppliedFold[MailingList, Address] = mailingList.focus(_.users.each).andThen(Getter[User, Address](_.address)) + val newLens: AppliedFold[MailingList, Int] = userFold.refocus(_.streetNumber) + val streetNumbers = newLens.getAll + + assertEquals(streetNumbers, List(12)) + } + + test("Getter refocus correctly composes Lens") { + val addressLens: Getter[User, Address] = Getter[User, Address](_.address) + val newLens: Getter[User, Int] = addressLens.refocus(_.streetNumber) + val streetNumber = newLens.get(elise) + + assertEquals(streetNumber, 12) + } + + test("AppliedGetter refocus correctly composes Lens") { + val addressLens: AppliedGetter[User, Address] = AppliedGetter(elise, Getter[User, Address](_.address)) + val newLens: AppliedGetter[User, Int] = addressLens.refocus(_.streetNumber) + val streetNumber = newLens.get + + assertEquals(streetNumber, 12) + } + + test("Setter refocus correctly composes Lens") { + val addressLens: Setter[User, Address] = Setter(f => user => user.copy(address = f(user.address))) + val newLens: Setter[User, Int] = addressLens.refocus(_.streetNumber) + val newElise = newLens.replace(50)(elise) + + assertEquals(newElise.address.streetNumber, 50) + } + + test("AppliedSetter refocus correctly composes Lens") { + val addressLens: AppliedSetter[User, Address] = AppliedSetter(elise, Setter(f => user => user.copy(address = f(user.address)))) + val newLens: AppliedSetter[User, Int] = addressLens.refocus(_.streetNumber) + val newElise = newLens.replace(50) + + assertEquals(newElise.address.streetNumber, 50) + } +}