Skip to content

Commit 01a952b

Browse files
authored
Refocus feature (#1184)
* Refocus works for unapplied Lens and Iso * WIP * Refocus feature * Refactored most of the refocus code away
1 parent 2dca0d1 commit 01a952b

File tree

5 files changed

+203
-3
lines changed

5 files changed

+203
-3
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package monocle
22

3-
import monocle.syntax.AppliedFocusSyntax
3+
import monocle.syntax.{AppliedFocusSyntax, ComposedFocusSyntax}
44
import monocle.internal.focus.FocusImpl
55
import monocle.function.{Each, At, Index}
66

7-
object Focus extends AppliedFocusSyntax {
7+
object Focus extends AppliedFocusSyntax with ComposedFocusSyntax {
88

99
sealed trait KeywordContext {
1010
extension [From] (from: From)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package monocle.internal.focus
2+
3+
import monocle.{Focus, Lens, Iso, Prism, Optional, Traversal, Getter, Setter, Fold, AppliedSetter, AppliedFold, AppliedGetter}
4+
import scala.quoted.{Type, Expr, Quotes, quotes}
5+
6+
private[monocle] object ComposedFocusImpl {
7+
8+
type AnyOptic[S,A] = Setter[S,A] | Fold[S,A] | AppliedSetter[S,A] | AppliedFold[S,A]
9+
10+
def apply[S: Type, A: Type, Next: Type](optic: Expr[AnyOptic[S,A]], lambda: Expr[Focus.KeywordContext ?=> A => Next])(using Quotes): Expr[Any] = {
11+
import quotes.reflect._
12+
13+
val generatedOptic = FocusImpl(lambda).asTerm
14+
val opticType = optic.asTerm.tpe.widen
15+
val nextType = TypeRepr.of[Next]
16+
val singleTypeParam: Boolean =
17+
opticType =:= TypeRepr.of[Fold[S,A]] ||
18+
opticType =:= TypeRepr.of[Getter[S,A]] ||
19+
opticType =:= TypeRepr.of[AppliedFold[S,A]] ||
20+
opticType =:= TypeRepr.of[AppliedGetter[S,A]]
21+
22+
val typeParams = if (singleTypeParam) List(nextType) else List(nextType, nextType)
23+
24+
Select.overloaded(optic.asTerm, "andThen", typeParams, List(generatedOptic)).asExpr
25+
}
26+
}

core/shared/src/main/scala-3.x/monocle/syntax/All.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ package monocle.syntax
22

33
object all extends Syntaxes
44

5-
trait Syntaxes extends AppliedSyntax with AppliedFocusSyntax with MacroSyntax with FieldsSyntax
5+
trait Syntaxes extends AppliedSyntax with AppliedFocusSyntax with ComposedFocusSyntax with MacroSyntax with FieldsSyntax
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package monocle.syntax
2+
3+
import monocle._
4+
import monocle.internal.focus.ComposedFocusImpl
5+
6+
trait ComposedFocusSyntax {
7+
8+
extension [S, A, Next] (optic: Setter[S, A] | Fold[S,A] | AppliedSetter[S,A] | AppliedFold[S,A]) {
9+
transparent inline def refocus(inline lambda: (Focus.KeywordContext ?=> A => Next)): Any =
10+
${ComposedFocusImpl[S, A, Next]('optic, 'lambda)}
11+
}
12+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package monocle.focus
2+
3+
import monocle._
4+
import monocle.syntax.all._
5+
import monocle.syntax.{AppliedGetter, AppliedSetter}
6+
7+
8+
object ComposedFocusTest {
9+
enum Roof {
10+
case Tiles(numTiles: Int)
11+
case Thatch(color: Color)
12+
case Glass(tint: Option[String])
13+
}
14+
15+
case class Color(r: Int, g: Int, b: Int)
16+
17+
case class Mailbox(address: Address)
18+
case class User(name: String, address: Address)
19+
case class Street(name: String)
20+
case class Potato(count: Int)
21+
case class Address(streetNumber: Int, street: Option[Street], roof: Roof, potatoes: List[Potato])
22+
23+
case class MailingList(users: List[User])
24+
25+
val elise = User("Elise", Address(12, Some(Street("high street")), Roof.Tiles(999), (1 to 4).toList.map(Potato.apply)))
26+
val mailbox = Mailbox(Address(1, Some(Street("cherrytree lane")), Roof.Thatch(Color(255, 255, 0)), Nil))
27+
}
28+
29+
30+
final class ComposedFocusTest extends munit.FunSuite {
31+
32+
import ComposedFocusTest._
33+
34+
test("Lens refocus correctly composes Lens") {
35+
val addressLens: Lens[User, Address] = Focus[User](_.address)
36+
val newLens: Lens[User, Int] = addressLens.refocus(_.streetNumber)
37+
val newElise = newLens.replace(50)(elise)
38+
39+
assertEquals(newElise.address.streetNumber, 50)
40+
}
41+
42+
test("AppliedLens refocus correctly composes Lens") {
43+
val addressLens: AppliedLens[User, Address] = elise.focus(_.address)
44+
val newLens: AppliedLens[User, Int] = addressLens.refocus(_.streetNumber)
45+
val newElise = newLens.replace(50)
46+
47+
assertEquals(newElise.address.streetNumber, 50)
48+
}
49+
50+
test("Lens refocus correctly composes Prism") {
51+
val roofLens: Lens[User, Roof] = Focus[User](_.address.roof)
52+
val newLens: Optional[User, Roof.Tiles] = roofLens.refocus(_.as[Roof.Tiles])
53+
val newElise = newLens.replace(Roof.Tiles(3))(elise)
54+
55+
assertEquals(newElise.address.roof, Roof.Tiles(3))
56+
}
57+
58+
test("Lens refocus correctly composes Iso") {
59+
val addressLens: Lens[User, Address] = Focus[User](_.address)
60+
val newLens: Lens[User, Int] = addressLens.refocus(_.streetNumber)
61+
val newElise = newLens.replace(50)(elise)
62+
63+
assertEquals(newElise.address.streetNumber, 50)
64+
}
65+
66+
test("Lens refocus correctly composes Optional") {
67+
val addressLens: Lens[User, Address] = Focus[User](_.address)
68+
val newLens: Optional[User, String] = addressLens.refocus(_.street.some.name)
69+
val newElise = newLens.replace("Crunkley Ave")(elise)
70+
71+
assertEquals(newElise.address.street.map(_.name), Some("Crunkley Ave"))
72+
}
73+
74+
test("Lens refocus correctly composes Traversal") {
75+
val addressLens: Lens[User, Address] = Focus[User](_.address)
76+
val newLens: Traversal[User, Int] = addressLens.refocus(_.potatoes.each.count)
77+
val newElise = newLens.modify(_ + 1)(elise)
78+
79+
assertEquals(newElise.address.potatoes.map(_.count), List(2,3,4,5))
80+
}
81+
82+
test("Prism refocus correctly composes Lens") {
83+
val oldLens: Prism[Roof, Roof.Thatch] = Focus[Roof](_.as[Roof.Thatch])
84+
val newLens: Optional[Roof, Int] = oldLens.refocus(_.color.r)
85+
val newRoof = newLens.replace(77)(Roof.Thatch(Color(255, 255, 255)))
86+
87+
assertEquals(newRoof, Roof.Thatch(Color(77, 255, 255)))
88+
}
89+
90+
test("Prism refocus correctly composes Prism") {
91+
val oldLens: Prism[Roof, Roof.Glass] = Focus[Roof](_.as[Roof.Glass])
92+
val newLens: Prism[Roof, String] = oldLens.refocus(_.tint.some)
93+
val newRoof = newLens.replace("light")(Roof.Glass(Some("dark")))
94+
95+
assertEquals(newRoof, Roof.Glass(Some("light")))
96+
}
97+
98+
test("Prism refocus correctly composes Iso") {
99+
val oldLens: Prism[Roof, Roof.Tiles] = Focus[Roof](_.as[Roof.Tiles])
100+
val newLens: Prism[Roof, Int] = oldLens.refocus(_.numTiles)
101+
val newRoof = newLens.replace(100)(Roof.Tiles(3))
102+
103+
assertEquals(newRoof, Roof.Tiles(100))
104+
}
105+
106+
test("Prism refocus correctly composes Optional") {
107+
val oldLens: Prism[Roof, Roof.Glass] = Focus[Roof](_.as[Roof.Glass])
108+
val newLens: Optional[Roof, String] = oldLens.refocus(_.tint.some)
109+
val newRoof = newLens.replace("light")(Roof.Glass(Some("dark")))
110+
111+
assertEquals(newRoof, Roof.Glass(Some("light")))
112+
}
113+
114+
test("Fold refocus correctly composes Lens") {
115+
val userFold: Fold[MailingList, Address] = Focus[MailingList](_.users.each).andThen(Getter[User, Address](_.address))
116+
val newLens: Fold[MailingList, Int] = userFold.refocus(_.streetNumber)
117+
val streetNumbers = newLens.getAll(MailingList(List(elise)))
118+
119+
assertEquals(streetNumbers, List(12))
120+
}
121+
122+
test("AppliedFold refocus correctly composes Lens") {
123+
val mailingList = MailingList(List(elise))
124+
val userFold: AppliedFold[MailingList, Address] = mailingList.focus(_.users.each).andThen(Getter[User, Address](_.address))
125+
val newLens: AppliedFold[MailingList, Int] = userFold.refocus(_.streetNumber)
126+
val streetNumbers = newLens.getAll
127+
128+
assertEquals(streetNumbers, List(12))
129+
}
130+
131+
test("Getter refocus correctly composes Lens") {
132+
val addressLens: Getter[User, Address] = Getter[User, Address](_.address)
133+
val newLens: Getter[User, Int] = addressLens.refocus(_.streetNumber)
134+
val streetNumber = newLens.get(elise)
135+
136+
assertEquals(streetNumber, 12)
137+
}
138+
139+
test("AppliedGetter refocus correctly composes Lens") {
140+
val addressLens: AppliedGetter[User, Address] = AppliedGetter(elise, Getter[User, Address](_.address))
141+
val newLens: AppliedGetter[User, Int] = addressLens.refocus(_.streetNumber)
142+
val streetNumber = newLens.get
143+
144+
assertEquals(streetNumber, 12)
145+
}
146+
147+
test("Setter refocus correctly composes Lens") {
148+
val addressLens: Setter[User, Address] = Setter(f => user => user.copy(address = f(user.address)))
149+
val newLens: Setter[User, Int] = addressLens.refocus(_.streetNumber)
150+
val newElise = newLens.replace(50)(elise)
151+
152+
assertEquals(newElise.address.streetNumber, 50)
153+
}
154+
155+
test("AppliedSetter refocus correctly composes Lens") {
156+
val addressLens: AppliedSetter[User, Address] = AppliedSetter(elise, Setter(f => user => user.copy(address = f(user.address))))
157+
val newLens: AppliedSetter[User, Int] = addressLens.refocus(_.streetNumber)
158+
val newElise = newLens.replace(50)
159+
160+
assertEquals(newElise.address.streetNumber, 50)
161+
}
162+
}

0 commit comments

Comments
 (0)