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

FallibleTransformer derivation for coproducts #69

Merged
merged 6 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ object FallibleTransformer extends LowPriorityAccumulatingInstances {
F: Mode.FailFast[F]
): FallibleTransformer[F, Source, Dest] = DerivedTransformers.failFastProduct[F, Source, Dest]

inline given betweenCoproductsAccumulating[F[+x], Source, Dest](using
Source: Mirror.SumOf[Source],
Dest: Mirror.SumOf[Dest],
F: Mode.Accumulating[F]
): FallibleTransformer[F, Source, Dest] = DerivedTransformers.fallibleCoproduct[F, Source, Dest]

inline given betweenCoproductsFailFast[F[+x], Source, Dest](using
Source: Mirror.SumOf[Source],
Dest: Mirror.SumOf[Dest],
F: Mode.FailFast[F]
): FallibleTransformer[F, Source, Dest] = DerivedTransformers.fallibleCoproduct[F, Source, Dest]

given betweenOptions[F[+x], Source, Dest](using
transformer: FallibleTransformer[F, Source, Dest],
F: Mode[F]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ private[ducktape] object DerivedTransformers {
)(using Quotes): Expr[FallibleTransformer[F, Source, Dest]] =
'{ source => ${ FailFastProductTransformations.transform[F, Source, Dest](Source, Dest, F, 'source) } }

inline def fallibleCoproduct[F[+x], Source, Dest](using
F: Mode[F],
Source: Mirror.SumOf[Source],
Dest: Mirror.SumOf[Dest]
): FallibleTransformer[F, Source, Dest] =
${ deriveFallibleCoproductTransformerMacro[F, Source, Dest]('F, 'Source, 'Dest) }

def deriveFallibleCoproductTransformerMacro[F[+x]: Type, Source: Type, Dest: Type](
F: Expr[Mode[F]],
Source: Expr[Mirror.SumOf[Source]],
Dest: Expr[Mirror.SumOf[Dest]]
)(using Quotes): Expr[FallibleTransformer[F, Source, Dest]] =
'{ source => ${ FallibleCoproductTransformations.transform[F, Source, Dest](Source, Dest, F, 'source) } }

inline def accumulatingProduct[F[+x], Source, Dest](using
F: Mode.Accumulating[F],
Source: Mirror.ProductOf[Source],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package io.github.arainko.ducktape.internal.macros

import io.github.arainko.ducktape.fallible.{ FallibleTransformer, Mode }
import io.github.arainko.ducktape.function.FunctionArguments
import io.github.arainko.ducktape.internal.modules.MaterializedConfiguration.*
import io.github.arainko.ducktape.internal.modules.*

import scala.deriving.*
import scala.quoted.*

private[ducktape] object FallibleCoproductTransformations {

def transform[F[+x]: Type, Source: Type, Dest: Type](
Source: Expr[Mirror.SumOf[Source]],
Dest: Expr[Mirror.SumOf[Dest]],
F: Expr[Mode[F]],
sourceValue: Expr[Source]
)(using Quotes): Expr[F[Dest]] = {
given Cases.Source = Cases.Source.fromMirror(Source)
given Cases.Dest = Cases.Dest.fromMirror(Dest)

val ifBranches = coproductBranches[F, Source, Dest](sourceValue, Cases.source.value, F)
ifStatement(ifBranches).asExprOf[F[Dest]]
}

private def coproductBranches[F[+x]: Type, Source: Type, Dest: Type](
sourceValue: Expr[Source],
sourceCases: List[Case],
F: Expr[Mode[F]]
)(using Quotes, Cases.Dest) = {
import quotes.reflect.*

sourceCases.map { source =>
source -> Cases.dest
.get(source.name)
.getOrElse(Failure.emit(Failure.NoChildMapping(source.name, summon[Type[Dest]])))
}.map { (source, dest) =>
val cond = IsInstanceOf(sourceValue, source.tpe)

(source.tpe -> dest.tpe) match {
case '[src] -> '[dest] =>
val value =
source.fallibleTransformerTo[F](dest).map {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's an optimization we can do here where we match on the summoned transformer and if it is a FallibleTransformer.fallibleFromTotal we can strip apart the total transformer with LiftTransformation.liftTransformation which will make it so that the body of the transformer is inlined instead of creating a transformer instance at runtime. Example of how to do it is here:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've achieved what you had suggested. I had problems getting a test case to match FallibleTransformer.fallibleFromTotal. But I've added an optimization to use total transformers, if available. The cost for that optimization is a second implicit search. I'm not sure if that's worth it.

case '{ $transformer: FallibleTransformer[F, src, dest] } =>
'{
val castedSource = $sourceValue.asInstanceOf[src]
val t = $transformer
t.transform(castedSource)
}.asExprOf[F[Dest]]
} match {
case Right(value) => value
case Left(explanation) =>
dest.materializeSingleton.map { singleton =>
'{ $F.pure[dest]($singleton.asInstanceOf[dest]) }
}
.getOrElse(
Failure.emit(
Failure.CannotTransformCoproductCaseFallible(summon[Type[F]], source.tpe, dest.tpe, explanation)
)
)
}
IfBranch(cond, value)
}
}
}

private def ifStatement(using Quotes)(branches: List[IfBranch]): quotes.reflect.Term = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we use duplicates of this, IfBranch and IsInstanceOf inside CoproductTransformations as well, would you mind extracting these into a separate and reusable object? eg. IfStatement that'd house IfBranch and other things related to this inside

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've extracted the IfBranch logic into its own reusable object ExhaustiveCoproductMatching. Maybe I've went a bit to far there. What do you say?

import quotes.reflect.*

branches match {
case IfBranch(cond, value) :: xs =>
If(cond.asTerm, value.asTerm, ifStatement(xs))
case Nil =>
'{ throw RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation") }.asTerm
}
}

private def IsInstanceOf(value: Expr[Any], tpe: Type[?])(using Quotes) =
tpe match {
case '[tpe] => '{ $value.isInstanceOf[tpe] }
}

private case class IfBranch(cond: Expr[Boolean], value: Expr[Any])
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.arainko.ducktape.internal.modules

import io.github.arainko.ducktape.Transformer
import io.github.arainko.ducktape.fallible.{ FallibleTransformer, Mode }

import scala.quoted.*

Expand All @@ -21,6 +22,20 @@ private[ducktape] final case class Case(
}
}

def fallibleTransformerTo[F[+x]](
that: Case
)(using quotes: Quotes, F: Type[F]): Either[String, Expr[FallibleTransformer[F, ?, ?]]] = {
import quotes.reflect.*

(tpe -> that.tpe) match {
case '[src] -> '[dest] =>
Implicits.search(TypeRepr.of[FallibleTransformer[F, src, dest]]) match {
case success: ImplicitSearchSuccess => Right(success.tree.asExprOf[FallibleTransformer[F, src, dest]])
case err: ImplicitSearchFailure => Left(err.explanation)
}
}
}

def materializeSingleton(using Quotes): Option[Expr[Any]] = {
import quotes.reflect.*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,22 @@ private[ducktape] object Failure {
""".stripMargin
}

final case class CannotTransformCoproductCaseFallible(
wrapperTpe: Type[?],
source: Type[?],
dest: Type[?],
implicitSearchExplanation: String
) extends Failure {
override final def render(using Quotes): String =
s"""
|Neither an instance of Transformer.Fallible[${wrapperTpe.show}, ${source.fullName}, ${dest.fullName}] was found
|nor are '${source.show}' '${dest.show}' singletons with the same name.
|
|Compiler supplied explanation for the failed Transformer derivation (may or may not be helpful):
|$implicitSearchExplanation
""".stripMargin
}

final case class FieldSourceMatchesNoneOfDestFields(config: Expr[Any], fieldSourceTpe: Type[?], destTpe: Type[?])
extends Failure {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package io.github.arainko.ducktape.internal.macros

import io.github.arainko.ducktape.fallible.FallibleTransformer
import io.github.arainko.ducktape.{ DucktapeSuite, Transformer }

import scala.deriving.Mirror

class AccumulatingCoproductTransformationsSuite extends DucktapeSuite {
type ErrorsOrResult = [X] =>> Either[List[String], X]

given Transformer.Mode.Accumulating[ErrorsOrResult] =
Transformer.Mode.Accumulating.either[String, List]

given deriveMandatoryOptionTransformer[A, B](using
transformer: FallibleTransformer[ErrorsOrResult, A, B]
): FallibleTransformer[ErrorsOrResult, Option[A], B] = {
case Some(a) => transformer.transform(a)
case None => Left(List("Missing required field"))
}

test("Derive sum of products") {
sealed trait From
object From {
case class Product(field: Option[Int]) extends From
case class Product2(field: Option[Int]) extends From
}

sealed trait To
object To {
case class Product(field: Int) extends To
case class Product2(field: Int) extends To
}

val transformer = FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To]

assert(transformer.transform(From.Product(Some(1))) == Right(To.Product(1)))
assert(transformer.transform(From.Product(None)) == Left(List("Missing required field")))
assert(transformer.transform(From.Product2(Some(2))) == Right(To.Product2(2)))
}

test("Derive sum of singletons") {
sealed trait From
object From {
case object Singleton extends From
case object Singleton2 extends From
}

sealed trait To
object To {
case object Singleton extends To
case object Singleton2 extends To
}

val transformer = FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To]

assert(transformer.transform(From.Singleton) == Right(To.Singleton))
assert(transformer.transform(From.Singleton2) == Right(To.Singleton2))
}

test("Derive sum of singleton and product") {
sealed trait From
object From {
case class Product(field: Option[Int]) extends From
case object Singleton extends From
}

sealed trait To
object To {
case class Product(field: Int) extends To
case object Singleton extends To
}

val transformer = FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To]

assert(transformer.transform(From.Singleton) == Right(To.Singleton))
assert(transformer.transform(From.Product(Some(5))) == Right(To.Product(5)))
assert(transformer.transform(From.Product(None)) == Left(List("Missing required field")))
}

test("Accumulate problems") {
sealed trait From
object From {
case class Product(field1: Option[Int], field2: Option[String]) extends From
}

sealed trait To
object To {
case class Product(field1: Int, field2: String) extends To
}

val transformer = FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To]

assert(transformer.transform(From.Product(None, None)) == Left(List("Missing required field", "Missing required field")))
assert(transformer.transform(From.Product(Some(1), None)) == Left(List("Missing required field")))
assert(transformer.transform(From.Product(None, Some("s"))) == Left(List("Missing required field")))
assert(transformer.transform(From.Product(Some(1), Some("s"))) == Right(To.Product(1, "s")))
}

test("Derivation fails if the case names don't align") {
sealed trait From
object From {
case class Product() extends From
}

sealed trait To
object To {
case class ProductWrongName() extends To
}

assertFailsToCompileWith {
"FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To]"
}("No child named 'Product' found in To")
}

test("Derivation fails if a case can't be transformed") {
sealed trait From
object From {
case class Product(s: String) extends From
}

sealed trait To
object To {
case class Product(y: String) extends To
}

assertFailsToCompileWith {
"FallibleTransformer.betweenCoproductsAccumulating[ErrorsOrResult, From, To]"
}(
"Neither an instance of Transformer.Fallible[ErrorsOrResult, From.Product, To.Product] was found\nnor are 'Product' 'Product' singletons with the same name"
)
}
}
Loading