Skip to content

Commit 834d402

Browse files
authored
[Focus] Each feature (#1072)
1 parent deaa44e commit 834d402

File tree

11 files changed

+118
-62
lines changed

11 files changed

+118
-62
lines changed

Diff for: core/shared/src/main/scala-3.x/monocle/Focus.scala

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package monocle
22

3+
import monocle.function.Each
34
import monocle.internal.focus.{FocusImpl, AppliedFocusImpl}
45
import monocle.syntax.FocusSyntax
56

Diff for: core/shared/src/main/scala-3.x/monocle/internal/focus/FocusBase.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ private[focus] trait FocusBase {
1414
case FieldSelect(name: String, fromType: TypeRepr, fromTypeArgs: List[TypeRepr], toType: TypeRepr)
1515
case OptionSome(toType: TypeRepr)
1616
case CastAs(fromType: TypeRepr, toType: TypeRepr)
17+
case Each(fromType: TypeRepr, toType: TypeRepr, eachInstance: Term)
1718

1819
override def toString(): String = this match {
1920
case FieldSelect(name, fromType, fromTypeArgs, toType) => s"FieldSelect($name, ${fromType.show}, ${fromTypeArgs.map(_.show)}, ${toType.show})"
2021
case OptionSome(toType) => s"OptionSome(${toType.show})"
2122
case CastAs(fromType, toType) => s"CastAs(${fromType.show}, ${toType.show})"
23+
case Each(fromType, toType, eachInstance) => s"Each(${fromType.show}, ${toType.show}, ...)"
2224
}
2325
}
2426

@@ -40,5 +42,4 @@ private[focus] trait FocusBase {
4042
}
4143

4244
type FocusResult[+A] = Either[FocusError, A]
43-
type ParseResult = FocusResult[List[FocusAction]]
4445
}

Diff for: core/shared/src/main/scala-3.x/monocle/internal/focus/GeneratorLoop.scala

+8-52
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ package monocle.internal.focus
33
import monocle.internal.focus.features.fieldselect.FieldSelectGenerator
44
import monocle.internal.focus.features.optionsome.OptionSomeGenerator
55
import monocle.internal.focus.features.castas.CastAsGenerator
6-
import monocle.{Lens, Iso, Prism, Optional}
6+
import monocle.internal.focus.features.each.EachGenerator
7+
import monocle.{Lens, Iso, Prism, Optional, Traversal}
78
import scala.quoted.Type
89

910

@@ -12,6 +13,7 @@ private[focus] trait AllGenerators
1213
with FieldSelectGenerator
1314
with OptionSomeGenerator
1415
with CastAsGenerator
16+
with EachGenerator
1517

1618
private[focus] trait GeneratorLoop {
1719
this: FocusBase with AllGenerators =>
@@ -31,60 +33,14 @@ private[focus] trait GeneratorLoop {
3133
case FocusAction.FieldSelect(name, fromType, fromTypeArgs, toType) => generateFieldSelect(name, fromType, fromTypeArgs, toType)
3234
case FocusAction.OptionSome(toType) => generateOptionSome(toType)
3335
case FocusAction.CastAs(fromType, toType) => generateCastAs(fromType, toType)
36+
case FocusAction.Each(fromType, toType, eachInstance) => generateEach(fromType, toType, eachInstance)
3437
}
3538

3639
private def composeOptics(lens1: Term, lens2: Term): FocusResult[Term] = {
37-
(lens1.tpe.asType, lens2.tpe.asType) match {
38-
case ('[Lens[from1, to1]], '[Lens[from2, to2]]) =>
39-
Right('{ ${lens1.asExprOf[Lens[from1, to1]]}.andThen(${lens2.asExprOf[Lens[to1, to2]]}) }.asTerm)
40-
41-
case ('[Lens[from1, to1]], '[Prism[from2, to2]]) =>
42-
Right('{ ${lens1.asExprOf[Lens[from1, to1]]}.andThen(${lens2.asExprOf[Prism[to1, to2]]}) }.asTerm)
43-
44-
case ('[Lens[from1, to1]], '[Optional[from2, to2]]) =>
45-
Right('{ ${lens1.asExprOf[Lens[from1, to1]]}.andThen(${lens2.asExprOf[Optional[to1, to2]]}) }.asTerm)
46-
47-
case ('[Lens[from1, to1]], '[Iso[from2, to2]]) =>
48-
Right('{ ${lens1.asExprOf[Lens[from1, to1]]}.andThen(${lens2.asExprOf[Iso[to1, to2]]}) }.asTerm)
49-
50-
case ('[Prism[from1, to1]], '[Prism[from2, to2]]) =>
51-
Right('{ ${lens1.asExprOf[Prism[from1, to1]]}.andThen(${lens2.asExprOf[Prism[to1, to2]]}) }.asTerm)
52-
53-
case ('[Prism[from1, to1]], '[Lens[from2, to2]]) =>
54-
Right('{ ${lens1.asExprOf[Prism[from1, to1]]}.andThen(${lens2.asExprOf[Lens[to1, to2]]}) }.asTerm)
55-
56-
case ('[Prism[from1, to1]], '[Optional[from2, to2]]) =>
57-
Right('{ ${lens1.asExprOf[Prism[from1, to1]]}.andThen(${lens2.asExprOf[Optional[to1, to2]]}) }.asTerm)
58-
59-
case ('[Prism[from1, to1]], '[Iso[from2, to2]]) =>
60-
Right('{ ${lens1.asExprOf[Prism[from1, to1]]}.andThen(${lens2.asExprOf[Iso[to1, to2]]}) }.asTerm)
61-
62-
case ('[Optional[from1, to1]], '[Lens[from2, to2]]) =>
63-
Right('{ ${lens1.asExprOf[Optional[from1, to1]]}.andThen(${lens2.asExprOf[Lens[to1, to2]]}) }.asTerm)
64-
65-
case ('[Optional[from1, to1]], '[Optional[from2, to2]]) =>
66-
Right('{ ${lens1.asExprOf[Optional[from1, to1]]}.andThen(${lens2.asExprOf[Optional[to1, to2]]}) }.asTerm)
67-
68-
case ('[Optional[from1, to1]], '[Prism[from2, to2]]) =>
69-
Right('{ ${lens1.asExprOf[Optional[from1, to1]]}.andThen(${lens2.asExprOf[Prism[to1, to2]]}) }.asTerm)
70-
71-
case ('[Optional[from1, to1]], '[Iso[from2, to2]]) =>
72-
Right('{ ${lens1.asExprOf[Optional[from1, to1]]}.andThen(${lens2.asExprOf[Iso[to1, to2]]}) }.asTerm)
73-
74-
case ('[Iso[from1, to1]], '[Lens[from2, to2]]) =>
75-
Right('{ ${lens1.asExprOf[Iso[from1, to1]]}.andThen(${lens2.asExprOf[Lens[to1, to2]]}) }.asTerm)
76-
77-
case ('[Iso[from1, to1]], '[Iso[from2, to2]]) =>
78-
Right('{ ${lens1.asExprOf[Iso[from1, to1]]}.andThen(${lens2.asExprOf[Iso[to1, to2]]}) }.asTerm)
79-
80-
case ('[Iso[from1, to1]], '[Optional[from2, to2]]) =>
81-
Right('{ ${lens1.asExprOf[Iso[from1, to1]]}.andThen(${lens2.asExprOf[Optional[to1, to2]]}) }.asTerm)
82-
83-
case ('[Iso[from1, to1]], '[Prism[from2, to2]]) =>
84-
Right('{ ${lens1.asExprOf[Iso[from1, to1]]}.andThen(${lens2.asExprOf[Prism[to1, to2]]}) }.asTerm)
85-
86-
case ('[a], '[b]) =>
87-
FocusError.ComposeMismatch(TypeRepr.of[a].show, TypeRepr.of[b].show).asResult
40+
lens2.tpe.widen match {
41+
// Won't yet work for polymorphism where A != B, nor for non-polymorphic optics Getter, Setter or Fold.
42+
case AppliedType(_, List(_, toType2)) => Right(Select.overloaded(lens1, "andThen", List(toType2, toType2), List(lens2)))
43+
case _ => FocusError.ComposeMismatch(lens1.tpe.show, lens2.tpe.show).asResult
8844
}
8945
}
9046
}

Diff for: core/shared/src/main/scala-3.x/monocle/internal/focus/ParserLoop.scala

+9-4
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@ import scala.quoted.Type
44
import monocle.internal.focus.features.fieldselect.FieldSelectParser
55
import monocle.internal.focus.features.optionsome.OptionSomeParser
66
import monocle.internal.focus.features.castas.CastAsParser
7+
import monocle.internal.focus.features.each.EachParser
78

89
private[focus] trait AllParsers
910
extends FocusBase
1011
with FieldSelectParser
1112
with OptionSomeParser
1213
with CastAsParser
14+
with EachParser
1315

1416
private[focus] trait ParserLoop {
1517
this: FocusBase with AllParsers =>
1618

1719
import macroContext.reflect._
1820

19-
def parseLambda[From: Type](lambda: Term): ParseResult = {
21+
def parseLambda[From: Type](lambda: Term): FocusResult[List[FocusAction]] = {
2022
val fromTypeIsConcrete = TypeRepr.of[From].classSymbol.isDefined
2123

2224
lambda match {
@@ -35,8 +37,8 @@ private[focus] trait ParserLoop {
3537
}
3638
}
3739

38-
private def parseLambdaBody(params: ParseParams): ParseResult = {
39-
def loop(remainingBody: Term, listSoFar: List[FocusAction]): ParseResult = {
40+
private def parseLambdaBody(params: ParseParams): FocusResult[List[FocusAction]] = {
41+
def loop(remainingBody: Term, listSoFar: List[FocusAction]): FocusResult[List[FocusAction]] = {
4042

4143
remainingBody match {
4244
case LambdaArgument(idName) if idName == params.argName => Right(listSoFar)
@@ -45,6 +47,9 @@ private[focus] trait ParserLoop {
4547
case OptionSome(Right(remainingCode, action)) => loop(remainingCode, action :: listSoFar)
4648
case OptionSome(Left(error)) => Left(error)
4749

50+
case Each(Right(remainingCode, action)) => loop(remainingCode, action :: listSoFar)
51+
case Each(Left(error)) => Left(error)
52+
4853
case CastAs(Right(remainingCode, action)) => loop(remainingCode, action :: listSoFar)
4954
case CastAs(Left(error)) => Left(error)
5055

@@ -56,7 +61,7 @@ private[focus] trait ParserLoop {
5661
}
5762
loop(params.lambdaBody, Nil)
5863
}
59-
64+
6065
private def unwrap(term: Term): Term = {
6166
term match {
6267
case Block(List(), inner) => unwrap(inner)

Diff for: core/shared/src/main/scala-3.x/monocle/internal/focus/features/castas/CastAsGenerator.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package monocle.internal.focus.features.castas
22

3+
import monocle.Prism
34
import monocle.internal.focus.FocusBase
45

56
private[focus] trait CastAsGenerator {
@@ -10,8 +11,8 @@ private[focus] trait CastAsGenerator {
1011
def generateCastAs(fromType: TypeRepr, toType: TypeRepr): Term = {
1112
(fromType.asType, toType.asType) match {
1213
case ('[f], '[t]) => '{
13-
_root_.monocle.Prism[f, t]((from: f) => if (from.isInstanceOf[t]) Some(from.asInstanceOf[t]) else None)
14-
((to: t) => to.asInstanceOf[f]) }.asTerm
14+
Prism[f, t]((from: f) => if (from.isInstanceOf[t]) Some(from.asInstanceOf[t]) else None)
15+
((to: t) => to.asInstanceOf[f]) }.asTerm
1516
}
1617
}
1718
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package monocle.internal.focus.features.each
2+
3+
import monocle.function.Each
4+
import monocle.internal.focus.FocusBase
5+
6+
private[focus] trait EachGenerator {
7+
this: FocusBase =>
8+
9+
import macroContext.reflect._
10+
11+
def generateEach(fromType: TypeRepr, toType: TypeRepr, eachInstance: Term): Term =
12+
(fromType.asType, toType.asType) match {
13+
case ('[f], '[t]) => '{(${eachInstance.asExprOf[Each[f, t]]}.each)}.asTerm
14+
}
15+
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package monocle.internal.focus.features.each
2+
3+
import monocle.internal.focus.FocusBase
4+
5+
private[focus] trait EachParser {
6+
this: FocusBase =>
7+
8+
import macroContext.reflect._
9+
10+
object Each extends FocusParser {
11+
12+
def unapply(term: Term): Option[FocusResult[(Term, FocusAction)]] = term match {
13+
case Apply(Apply(TypeApply(Ident("each"), List(_, toTypeTree)), List(remainingCode)), List(eachInstance)) =>
14+
val fromType = remainingCode.tpe.widen
15+
val toType = toTypeTree.tpe
16+
val action = FocusAction.Each(fromType, toType, eachInstance)
17+
Some(Right(remainingCode, action))
18+
19+
case _ => None
20+
}
21+
}
22+
}

Diff for: core/shared/src/main/scala-3.x/monocle/internal/focus/features/fieldselect/FieldSelectGenerator.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ private[focus] trait FieldSelectGenerator {
1919
val getter: f => t = (from: f) =>
2020
${ generateGetter(field, '{from}.asTerm).asExprOf[t] }
2121

22-
_root_.monocle.Lens.apply[f, t](getter)(setter)
22+
Lens.apply[f, t](getter)(setter)
2323
}.asTerm
2424
}
2525
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package monocle.internal.focus.features.optionsome
22

33
import monocle.internal.focus.FocusBase
4+
import monocle.std.option.some
45

56
private[focus] trait OptionSomeGenerator {
67
this: FocusBase =>
@@ -9,7 +10,7 @@ private[focus] trait OptionSomeGenerator {
910

1011
def generateOptionSome(toType: TypeRepr): Term = {
1112
toType.asType match {
12-
case '[t] => '{ _root_.monocle.std.option.some[t] }.asTerm
13+
case '[t] => '{ some[t] }.asTerm
1314
}
1415
}
1516
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package monocle.syntax
22

3+
import monocle.function.Each
4+
35
trait FocusSyntax {
46
extension [CastTo] (from: Any)
57
def as: CastTo = scala.sys.error("Extension method 'as[CastTo]' should only be used within the monocle.Focus macro.")
68

79
extension [A] (opt: Option[A])
8-
def some: A = scala.sys.error("Extension method 'some' should only be used within the monocle.Focus macro.")
10+
def some: A = scala.sys.error("Extension method 'some' should only be used within the monocle.Focus macro.")
11+
12+
extension [From, To] (from: From)(using Each[From, To])
13+
def each: To = scala.sys.error("Extension method 'each' should only be used within the monocle.Focus macro.")
914
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package monocle.focus
2+
3+
import monocle.Focus
4+
import monocle.Focus._
5+
import monocle.function.Each._
6+
import monocle.std.list._
7+
8+
final class FocusEachTest extends munit.FunSuite {
9+
10+
test("Direct each on the argument") {
11+
val eachNumber = Focus[List[Int]](_.each)
12+
val list = List(1,2,3)
13+
assertEquals(eachNumber.getAll(list), List(1,2,3))
14+
assertEquals(eachNumber.modify(_ + 1)(list), List(2,3,4))
15+
}
16+
17+
test("Each on a field") {
18+
case class School(name: String, students: List[Student])
19+
case class Student(firstName: String, lastName: String, yearLevel: Int)
20+
21+
22+
val school = School("Sparkvale Primary School", List(
23+
Student("Arlen", "Appleby", 5),
24+
Student("Bob", "Bobson", 6),
25+
Student("Carol", "Cornell", 7)
26+
))
27+
28+
val studentNames = Focus[School](_.students.each.firstName)
29+
val studentYears = Focus[School](_.students.each.yearLevel)
30+
31+
assertEquals(studentNames.getAll(school), List("Arlen", "Bob", "Carol"))
32+
}
33+
34+
test("Focus operator each commutes with standalone operator each") {
35+
case class School(name: String, students: List[Student])
36+
case class Student(firstName: String, lastName: String, yearLevel: Int)
37+
38+
val school = School("Sparkvale Primary School", List(
39+
Student("Arlen", "Appleby", 5),
40+
Student("Bob", "Bobson", 6),
41+
Student("Carol", "Cornell", 7)
42+
))
43+
44+
assertEquals(
45+
Focus[School](_.students.each).getAll(school),
46+
Focus[School](_.students).each.getAll(school))
47+
}
48+
}

0 commit comments

Comments
 (0)