Skip to content

Commit

Permalink
[Focus] Each feature (#1072)
Browse files Browse the repository at this point in the history
  • Loading branch information
kenbot committed Feb 17, 2021
1 parent deaa44e commit 834d402
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 62 deletions.
1 change: 1 addition & 0 deletions core/shared/src/main/scala-3.x/monocle/Focus.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package monocle

import monocle.function.Each
import monocle.internal.focus.{FocusImpl, AppliedFocusImpl}
import monocle.syntax.FocusSyntax

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}, ...)"
}
}

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

type FocusResult[+A] = Either[FocusError, A]
type ParseResult = FocusResult[List[FocusAction]]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -12,6 +13,7 @@ private[focus] trait AllGenerators
with FieldSelectGenerator
with OptionSomeGenerator
with CastAsGenerator
with EachGenerator

private[focus] trait GeneratorLoop {
this: FocusBase with AllGenerators =>
Expand All @@ -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.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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package monocle.internal.focus.features.castas

import monocle.Prism
import monocle.internal.focus.FocusBase

private[focus] trait CastAsGenerator {
Expand All @@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}

}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 =>
Expand All @@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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.")
}
48 changes: 48 additions & 0 deletions core/shared/src/test/scala-3.x/monocle/focus/FocusEachTest.scala
Original file line number Diff line number Diff line change
@@ -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))
}
}

0 comments on commit 834d402

Please sign in to comment.