Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Focus] Each feature #1072

Merged
merged 3 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.")
}
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))
}
}