Skip to content

Commit

Permalink
Companion object-based Contains derivation (fix #143)
Browse files Browse the repository at this point in the history
  • Loading branch information
danslapman committed Mar 16, 2024
1 parent 8cdbd60 commit 4190cc0
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 90 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ node_modules/
.bsp
*.db
.jvmopts
**/.DS_Store
11 changes: 10 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,16 @@ lazy val opticsMacro = project
)
opts.filterNot(opt => suppressed.exists(opt.contains))
},
name := "glass-macro"
name := "glass-macro",
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((3, _)) =>
Seq(
"io.github.kitlangton" %% "quotidian" % "0.0.14"
)
case _ => Seq.empty
}
}
)

lazy val coreModules =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/** a collection of classic monomorphic optics based on http://hackage.haskell.org/package/lens and
* http://julien-truffaut.github.io/Monocle/ using names readable for user unfamiliar with then capable for using as
* implicit evidences in effect transmogrification
*
* see hierarchy here: https://wiki.tcsbank.ru/display/API/optics
*/
package object glass {
type Same[A, B] = PSame[A, A, B, B]
Expand Down
27 changes: 27 additions & 0 deletions modules/core/src/main/scala-3/glass/glass.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package glass

/** a collection of classic monomorphic optics based on http://hackage.haskell.org/package/lens and
* http://julien-truffaut.github.io/Monocle/ using names readable for user unfamiliar with then capable for using as
* implicit evidences in effect transmogrification
*/

type Same[A, B] = PSame[A, A, B, B]
type Equivalent[A, B] = PEquivalent[A, A, B, B]
type Subset[A, B] = PSubset[A, A, B, B]
type Contains[A, B] = PContains[A, A, B, B]
type Property[A, B] = PProperty[A, A, B, B]
type Repeated[A, B] = PRepeated[A, A, B, B]
type Items[A, B] = PItems[A, A, B, B]
type Reduced[A, B] = PReduced[A, A, B, B]
type Downcast[A, B] = PDowncast[A, A, B, B]
type Upcast[A, B] = PUpcast[A, A, B, B]
type Extract[A, B] = PExtract[A, A, B, B]
type Folded[A, B] = PFolded[A, A, B, B]
type Update[A, B] = PUpdate[A, A, B, B]
type Zipping[A, B] = PZipping[A, A, B, B]

/** label provider for instance discrimination like Contains[A, B] with Label["first"]
*/
type Label[label] = Any {
type Label = label
}
10 changes: 10 additions & 0 deletions modules/macro/src/main/scala-3/glass/macros/DeriveContains.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package glass.macros

import glass.macros.internal.{CompanionClass, ContainsFor}

trait DeriveContains:
given conversion(using
cc: CompanionClass[this.type],
contains: ContainsFor[cc.Type]
): Conversion[this.type, contains.Out] =
_ => contains.contains
29 changes: 1 addition & 28 deletions modules/macro/src/main/scala-3/glass/macros/GenContains.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,4 @@ object GenContains:
def apply[A]: MkContains[A] = new MkContains[A]

class MkContains[A]:
inline def apply[B](inline expr: A => B): Contains[A, B] = ${ mkContains('expr) }

def mkContains[A: Type, B: Type](expr: Expr[A => B])(using qctx: Quotes) =
import qctx.reflect.*

val (_, term) = unwrapLambda(expr.asTerm)

term match {
case Select(CaseClass(cc), name) =>
val ccType = cc.tpe.widen.dealias
val fieldType =
ccType.classSymbol
.map(_.fieldMembers.find(_.name.trim == name).getOrElse(Symbol.noSymbol))
.flatMap {
case FieldType(ta) => Some(ta)
case _ => None
}
.getOrElse(quotes.reflect.report.errorAndAbort("Can't get field type"))

(ccType.asType, fieldType.asType) match {
case ('[a], '[b]) =>
'{
Contains[a]((from: a) => ${ Select.unique('{ from }.asTerm, name).asExprOf[b] })((from: a, to: b) =>
${ Select.overloaded('{ from }.asTerm, "copy", Nil, NamedArg(name, '{ to }.asTerm) :: Nil).asExprOf[a] }
)
}.asExprOf[Contains[A, B]]
}
}
inline def apply[B](inline expr: A => B): Contains[A, B] = ${ ContainsMacro.mkContainsImpl('expr) }
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package glass.macros.internal

import glass.*

private[macros] class ContainsSelector[A](lenses: Map[String, PContains[A, A, ?, ?]]) extends Selectable:
inline def selectDynamic(name: String): PContains[A, A, ?, ?] =
lenses(name)
89 changes: 89 additions & 0 deletions modules/macro/src/main/scala-3/glass/macros/internal/impl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package glass.macros.internal

import glass.*
import quotidian.*
import quotidian.syntax.*
import quoted.*

private[macros] object ContainsMacro:
def mkContainsImpl[A: Type, B: Type](expr: Expr[A => B])(using Quotes): Expr[PContains[A, A, B, B]] =
import quotes.reflect.*
expr.asTerm.underlyingArgument match
case Lambda(_, select @ Select(a, b)) =>
val productMirror = MacroMirror.summonProduct[A]

val elem = productMirror
.elemForSymbol(select.symbol)
.getOrElse(
report.errorAndAbort(s"Invalid selector ${select.show}, must be a field of ${productMirror.monoType.show}")
)
.asElemOf[B]

'{
new PContains[A, A, B, B]:
def extract(a: A) = ${ elem.get('a) }
def set(a: A, b: B) = ${ elem.set('a, 'b) }
}
case other => report.errorAndAbort(s"Expected a selector of the form `s => a`, but got: ${other}")

implicit transparent inline def makeContainses[S]: Any = ${ makeContainsesImpl[S] }

def makeContainsesImpl[S: Type](using Quotes): Expr[Any] =
import quotes.reflect.*

val productMirror = MacroMirror.summonProduct[S]

val containsMap = Expr.ofMap(productMirror.elems.map { elem =>
import elem.asType
val selector = '{ (s: S) => ${ elem.get('s) } }
elem.label -> mkContainsImpl(selector)
})

val refinedType =
Refinement.of[ContainsSelector[S]](
productMirror.elemsWithTypes.map { case (elem, '[a]) =>
elem.label -> TypeRepr.of[PContains[S, S, a, a]]
}
)

refinedType.asType match
case '[t] =>
'{ new ContainsSelector[S]($containsMap).asInstanceOf[t] }

private[macros] trait ContainsFor[A]:
type Out

def contains: Out

private[macros] object ContainsFor:
transparent inline given derived[A]: ContainsFor[A] = ${ containsForImpl[A] }

private def containsForImpl[A: Type](using Quotes) =
import quotes.reflect.*
val lensesExpr = ContainsMacro.makeContainsesImpl[A]
lensesExpr.asTerm.tpe.asType match
case '[t] =>
'{
new ContainsFor[A]:
type Out = t

def contains: t = $lensesExpr.asInstanceOf[t]
}

private[macros] trait CompanionClass[A]:
type Type

private[macros] object CompanionClass:
transparent inline given [A]: CompanionClass[A] = ${ companionImpl[A] }

private def companionImpl[A: Type](using Quotes) =
import quotes.reflect.*
val companionClass = TypeRepr.companionClassOf[A]
if companionClass.typeSymbol.isNoSymbol then report.errorAndAbort(s"No companion class found for ${Type.show[A]}")

TypeRepr.companionClassOf[A].asType match
case '[t] =>
'{
new CompanionClass[A]:
type Type = t
}
53 changes: 0 additions & 53 deletions modules/macro/src/main/scala-3/glass/macros/internal/utils.scala

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,17 @@ import org.scalatest.matchers.should.Matchers.shouldBe

class GenContainsSpec extends AnyFunSuite:
test("Get") {
val lens = GenContains[Bar](_.i)

val data = Bar(42)

val sut = lens.get(data)
val sut = Bar.i.get(data)

sut shouldBe 42
}

test("Set") {
val lens = GenContains[Bar](_.i)

val data = Bar(42)

val sut = lens.update(data, _ + 1)
val sut = Bar.i.update(data, _ + 1)

sut.i shouldBe 43
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ case object B extends A
case object C extends A

case class Bar(i: Int)
object Bar extends DeriveContains

0 comments on commit 4190cc0

Please sign in to comment.