Skip to content
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
31 changes: 28 additions & 3 deletions modules/benchmarks/src/main/scala/difflicious/DiffBench.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package difflicious

import java.util.concurrent.TimeUnit

import difflicious.differ.ValueDiffer
import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.infra.Blackhole

Expand All @@ -13,18 +14,34 @@ class DiffBench {
val big = Big(
12,
"asdfa",
Map(Key("a", 1) -> Dog("fido", 3.5)),
Vector(Dog("spot", 7.0)),
Set(Dog("rex", 2.0)),
)

val bigClone = Big(
12,
"asdfa",
Map(Key("a", 1) -> Dog("fido", 3.5)),
Vector(Dog("spot", 7.0)),
Set(Dog("rex", 2.0)),
)

@Benchmark
def usingDiff(bh: Blackhole): Unit = {
bh.consume(Big.diff.diff(big, bigClone))
}

@Benchmark
def usingDiffObtainedOnly(bh: Blackhole): Unit = {
bh.consume(Big.diff.diff(DiffInput.ObtainedOnly(big)))
}

@Benchmark
def usingDiffExpectedOnly(bh: Blackhole): Unit = {
bh.consume(Big.diff.diff(DiffInput.ExpectedOnly(bigClone)))
}

@Benchmark
def usingEquals(bh: Blackhole): Unit = {
bh.consume(big == bigClone)
Expand All @@ -34,9 +51,9 @@ class DiffBench {
final case class Big(
i: Int,
s: String,
// map: Map[Key, Dog],
// list: Vector[Dog],
// set: Set[Dog],
map: Map[Key, Dog],
list: Vector[Dog],
set: Set[Dog],
)

object Big {
Expand All @@ -48,7 +65,15 @@ final case class Dog(
age: Double,
)

object Dog {
implicit val diff: Differ[Dog] = Differ.derived[Dog]
}

final case class Key(
name: String,
x: Int,
)

object Key {
implicit val diff: ValueDiffer[Key] = Differ.useEquals[Key](_.toString)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import hearth.fp.effect.*
import hearth.fp.instances.*
import hearth.fp.syntax.*

import scala.collection.immutable.ListMap
import scala.util.control.NoStackTrace

private[difflicious] trait DifferDerivationMacros { this: hearth.MacroCommons =>
Expand Down Expand Up @@ -85,28 +84,24 @@ private[difflicious] trait DifferDerivationMacros { this: hearth.MacroCommons =>
field.value.asInstanceOf[Method[A, Field]].isConstructorArgument
}

val fields = constructorFields.zipWithIndex.toList.parTraverse { case (field, index) =>
val fields = constructorFields.toList.parTraverse { field =>
import field.Underlying as Field

val fieldName = decodedName(field.value.name)
val getter = productGetterFor[A](index)
summonFieldDiffer[A, Field](deriveIfMissing).map { differ =>
Expr.quote {
Tuple2(
Expr.splice(Expr(fieldName)),
Tuple2(
Expr.splice(getter),
Expr.splice(differ).asInstanceOf[difflicious.Differ[Any]],
),
Expr.splice(differ).asInstanceOf[difflicious.Differ[Any]],
)
}
}
}

fields.map { fields =>
Expr.quote {
new difflicious.differ.RecordDiffer[A](
fieldDiffers = Expr.splice(listMapExpr[A](fields)),
new difflicious.differ.ProductDiffer[A](
fieldDiffers = Expr.splice(fieldDiffersExpr(fields)),
isIgnored = false,
typeName = Expr.splice(typeNameExpr[A]),
)
Expand Down Expand Up @@ -642,19 +637,15 @@ private[difflicious] trait DifferDerivationMacros { this: hearth.MacroCommons =>
renderDerivationErrorLines(errors, s"$indent ")
}

private def productGetterFor[A: Type](index: Int): Expr[A => Any] =
Expr.quote { (value: A) =>
value.asInstanceOf[scala.Product].productElement(Expr.splice(Expr(index)))
}
private def fieldDiffersExpr(
fields: List[Expr[(String, Differ[Any])]],
): Expr[Vector[(String, Differ[Any])]] = {
implicit val fieldDifferPair: Type[(String, Differ[Any])] = fieldDifferPairType

private def listMapExpr[A: Type](
fields: List[Expr[(String, (A => Any, Differ[Any]))]],
): Expr[ListMap[String, (A => Any, Differ[Any])]] =
fields.foldLeft(Expr.quote(ListMap.empty[String, (A => Any, difflicious.Differ[Any])])) { (acc, field) =>
Expr.quote {
Expr.splice(acc) + Expr.splice(field)
}
Expr.quote {
Vector(Expr.splice(VarArgs(fields*))*)
}
}

private def vectorExpr[A: Type](
cases: List[Expr[OneOfDiffer.Case[A, Any]]],
Expand Down Expand Up @@ -693,6 +684,9 @@ private[difflicious] trait DifferDerivationMacros { this: hearth.MacroCommons =>
private def oneOfCaseTypeInstance[A: Type]: Type[OneOfDiffer.Case[A, Any]] =
Type.of[OneOfDiffer.Case[A, Any]]

private def fieldDifferPairType: Type[(String, Differ[Any])] =
Type.of[(String, Differ[Any])]

private def derivationCacheKey[A: Type]: String =
s"${Type[A].plainPrint}"

Expand Down
6 changes: 4 additions & 2 deletions modules/core/src/main/scala/difflicious/PairType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package difflicious
sealed trait PairType

object PairType {
sealed trait ObtainedOrExpected extends PairType

case object Both extends PairType
case object ObtainedOnly extends PairType
case object ExpectedOnly extends PairType
case object ObtainedOnly extends PairType with ObtainedOrExpected
case object ExpectedOnly extends PairType with ObtainedOrExpected
}
112 changes: 112 additions & 0 deletions modules/core/src/main/scala/difflicious/differ/ProductDiffer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package difflicious.differ

import difflicious.*
import difflicious.utils.TypeName.SomeTypeName

import scala.collection.immutable.ListMap

/** A differ for product data structures such as tuples and case classes.
*/
final class ProductDiffer[T](
fieldDiffers: Vector[(String, Differ[Any])],
isIgnored: Boolean,
typeName: SomeTypeName,
) extends Differ[T] {
override type R = DiffResult.RecordResult

override def diff(inputs: DiffInput[T]): R = inputs match {
case DiffInput.Both(obtained, expected) =>
val obtainedProduct = obtained.asInstanceOf[Product]
val expectedProduct = expected.asInstanceOf[Product]
val diffResultsBuilder = ListMap.newBuilder[String, DiffResult]
var isOk = true
var index = 0
val fieldCount = fieldDiffers.size

while (index < fieldCount) {
val fieldDiffer = fieldDiffers(index)
val diffResult =
fieldDiffer._2.diff(obtainedProduct.productElement(index), expectedProduct.productElement(index))
diffResultsBuilder += fieldDiffer._1 -> diffResult
if (!diffResult.isOk) isOk = false
index += 1
}

val diffResults = diffResultsBuilder.result()
DiffResult
.RecordResult(
typeName = typeName,
fields = diffResults,
pairType = PairType.Both,
isIgnored = isIgnored,
isOk = isIgnored || isOk,
)
case DiffInput.ObtainedOnly(value) =>
calcDiffResultForOneSide(
product = value.asInstanceOf[Product],
pairType = PairType.ObtainedOnly,
)
case DiffInput.ExpectedOnly(expected) =>
calcDiffResultForOneSide(
product = expected.asInstanceOf[Product],
pairType = PairType.ExpectedOnly,
)
}

private def calcDiffResultForOneSide(
product: Product,
pairType: PairType.ObtainedOrExpected,
): R = {
val diffResultsBuilder = ListMap.newBuilder[String, DiffResult]
var index = 0
val fieldCount = fieldDiffers.size

while (index < fieldCount) {
val fieldDiffer = fieldDiffers(index)
val fieldValue = product.productElement(index)
val diffInput: DiffInput[Any] = pairType match {
case PairType.ObtainedOnly => DiffInput.ObtainedOnly(fieldValue)
case PairType.ExpectedOnly => DiffInput.ExpectedOnly(fieldValue)
}
val diffResult = fieldDiffer._2.diff(diffInput)
diffResultsBuilder += fieldDiffer._1 -> diffResult
index += 1
}

DiffResult
.RecordResult(
typeName = typeName,
fields = diffResultsBuilder.result(),
pairType = pairType,
isIgnored = isIgnored,
isOk = isIgnored,
)
}

override def configureIgnored(newIgnored: Boolean): Differ[T] =
new ProductDiffer[T](fieldDiffers = fieldDiffers, isIgnored = newIgnored, typeName = typeName)

override def configurePath(
step: String,
nextPath: ConfigurePath,
op: ConfigureOp,
): Either[ConfigureError, Differ[T]] = {
val index = fieldDiffers.indexWhere(_._1 == step)

if (index < 0) {
Left(ConfigureError.NonExistentField(nextPath, "ProductDiffer"))
} else {
val (fieldName, fieldDiffer) = fieldDiffers(index)
fieldDiffer.configureRaw(nextPath, op).map { newFieldDiffer =>
new ProductDiffer[T](
fieldDiffers = fieldDiffers.updated(index, (fieldName, newFieldDiffer)),
isIgnored = isIgnored,
typeName = typeName,
)
}
}
}

override def configurePairBy(path: ConfigurePath, op: ConfigureOp.PairBy[?]): Either[ConfigureError, Differ[T]] =
Left(ConfigureError.InvalidConfigureOp(path, op, "ProductDiffer"))
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
lazy val derivedDiffer$macro$4: difflicious.Differ[difflicious.testtypes.RecursiveDerivedNode] = new difflicious.differ.LazyDiffer[difflicious.testtypes.RecursiveDerivedNode](new difflicious.differ.RecordDiffer[difflicious.testtypes.RecursiveDerivedNode](ListMap.empty[String, Tuple2[Function1[difflicious.testtypes.RecursiveDerivedNode, Any], difflicious.Differ[Any]]].+[Tuple2[Function1[difflicious.testtypes.RecursiveDerivedNode, Any], difflicious.Differ[Any]]](Tuple2.apply[String, Tuple2[Function1[difflicious.testtypes.RecursiveDerivedNode, Any], difflicious.Differ[Any]]]("value", Tuple2.apply[Function1[difflicious.testtypes.RecursiveDerivedNode, Any], difflicious.Differ[Any]](((value: difflicious.testtypes.RecursiveDerivedNode) => value.asInstanceOf[Product].productElement(0)), difflicious.Differ.stringDiffer.asInstanceOf[difflicious.Differ[Any]]))).+[Tuple2[Function1[difflicious.testtypes.RecursiveDerivedNode, Any], difflicious.Differ[Any]]](Tuple2.apply[String, Tuple2[Function1[difflicious.testtypes.RecursiveDerivedNode, Any], difflicious.Differ[Any]]]("children", Tuple2.apply[Function1[difflicious.testtypes.RecursiveDerivedNode, Any], difflicious.Differ[Any]](((value: difflicious.testtypes.RecursiveDerivedNode) => value.asInstanceOf[Product].productElement(1)), new difflicious.differ.SeqDiffer[List,difflicious.testtypes.RecursiveDerivedNode](false, difflicious.ConfigureOp.PairBy.Index, derivedDiffer$macro$4, difflicious.utils.TypeName.apply[List[difflicious.testtypes.RecursiveDerivedNode]](/*irrelevant*/), difflicious.utils.SeqLike.stdSeqAsSeq[[+A]List[A]]).asInstanceOf[difflicious.Differ[Any]]))), false, difflicious.utils.TypeName.apply[Any]("difflicious.testtypes.RecursiveDerivedNode", "RecursiveDerivedNode", Nil)));
lazy val derivedDiffer$macro$4: difflicious.Differ[difflicious.testtypes.RecursiveDerivedNode] = new difflicious.differ.LazyDiffer[difflicious.testtypes.RecursiveDerivedNode](new difflicious.differ.ProductDiffer[difflicious.testtypes.RecursiveDerivedNode](Vector.apply[Tuple2[String, difflicious.Differ[Any]]](Tuple2.apply[String, difflicious.Differ[Any]]("value", difflicious.Differ.stringDiffer.asInstanceOf[difflicious.Differ[Any]]), Tuple2.apply[String, difflicious.Differ[Any]]("children", new difflicious.differ.SeqDiffer[List,difflicious.testtypes.RecursiveDerivedNode](false, difflicious.ConfigureOp.PairBy.Index, derivedDiffer$macro$4, difflicious.utils.TypeName.apply[List[difflicious.testtypes.RecursiveDerivedNode]](/*irrelevant*/), difflicious.utils.SeqLike.stdSeqAsSeq[[+A]List[A]]).asInstanceOf[difflicious.Differ[Any]])), false, difflicious.utils.TypeName.apply[Any]("difflicious.testtypes.RecursiveDerivedNode", "RecursiveDerivedNode", Nil)));
derivedDiffer$macro$4
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
lazy val derivedDiffer$macro$5: difflicious.Differ[difflicious.testtypes.RecursiveDerivedDeepNode] = new difflicious.differ.LazyDiffer[difflicious.testtypes.RecursiveDerivedDeepNode](new difflicious.differ.RecordDiffer[difflicious.testtypes.RecursiveDerivedDeepNode](ListMap.empty[String, Tuple2[Function1[difflicious.testtypes.RecursiveDerivedDeepNode, Any], difflicious.Differ[Any]]].+[Tuple2[Function1[difflicious.testtypes.RecursiveDerivedDeepNode, Any], difflicious.Differ[Any]]](Tuple2.apply[String, Tuple2[Function1[difflicious.testtypes.RecursiveDerivedDeepNode, Any], difflicious.Differ[Any]]]("value", Tuple2.apply[Function1[difflicious.testtypes.RecursiveDerivedDeepNode, Any], difflicious.Differ[Any]](((value: difflicious.testtypes.RecursiveDerivedDeepNode) => value.asInstanceOf[Product].productElement(0)), difflicious.Differ.stringDiffer.asInstanceOf[difflicious.Differ[Any]]))).+[Tuple2[Function1[difflicious.testtypes.RecursiveDerivedDeepNode, Any], difflicious.Differ[Any]]](Tuple2.apply[String, Tuple2[Function1[difflicious.testtypes.RecursiveDerivedDeepNode, Any], difflicious.Differ[Any]]]("children", Tuple2.apply[Function1[difflicious.testtypes.RecursiveDerivedDeepNode, Any], difflicious.Differ[Any]](((value: difflicious.testtypes.RecursiveDerivedDeepNode) => value.asInstanceOf[Product].productElement(1)), new difflicious.differ.SeqDiffer[List,difflicious.testtypes.RecursiveDerivedDeepNode](false, difflicious.ConfigureOp.PairBy.Index, derivedDiffer$macro$5, difflicious.utils.TypeName.apply[List[difflicious.testtypes.RecursiveDerivedDeepNode]](/*irrelevant*/), difflicious.utils.SeqLike.stdSeqAsSeq[[+A]List[A]]).asInstanceOf[difflicious.Differ[Any]]))), false, difflicious.utils.TypeName.apply[Any]("difflicious.testtypes.RecursiveDerivedDeepNode", "RecursiveDerivedDeepNode", Nil)));
lazy val derivedDiffer$macro$5: difflicious.Differ[difflicious.testtypes.RecursiveDerivedDeepNode] = new difflicious.differ.LazyDiffer[difflicious.testtypes.RecursiveDerivedDeepNode](new difflicious.differ.ProductDiffer[difflicious.testtypes.RecursiveDerivedDeepNode](Vector.apply[Tuple2[String, difflicious.Differ[Any]]](Tuple2.apply[String, difflicious.Differ[Any]]("value", difflicious.Differ.stringDiffer.asInstanceOf[difflicious.Differ[Any]]), Tuple2.apply[String, difflicious.Differ[Any]]("children", new difflicious.differ.SeqDiffer[List,difflicious.testtypes.RecursiveDerivedDeepNode](false, difflicious.ConfigureOp.PairBy.Index, derivedDiffer$macro$5, difflicious.utils.TypeName.apply[List[difflicious.testtypes.RecursiveDerivedDeepNode]](/*irrelevant*/), difflicious.utils.SeqLike.stdSeqAsSeq[[+A]List[A]]).asInstanceOf[difflicious.Differ[Any]])), false, difflicious.utils.TypeName.apply[Any]("difflicious.testtypes.RecursiveDerivedDeepNode", "RecursiveDerivedDeepNode", Nil)));
derivedDiffer$macro$5
}
Loading