Skip to content

Instantly share code, notes, and snippets.

@Sedose
Last active September 10, 2024 10:26
Show Gist options
  • Save Sedose/2bc7f55e190678a25748449242f03ca4 to your computer and use it in GitHub Desktop.
Save Sedose/2bc7f55e190678a25748449242f03ca4 to your computer and use it in GitHub Desktop.
Counting Matching Non-Null Fields with Macro. Reduce code duplication

Introduction

This project uses Scala 3 macros to automate the process of comparing fields in case classes. The macro inspects the fields of two instances of the same case class and counts how many fields are non-null and have the same value. This approach reduces boilerplate code AND comes with the benefit of macro code generation, where the macro expands at compile time to generate the necessary comparison logic.

import scala.quoted.{quotes, Expr, Quotes, Type}
inline def compareEntity[T](a: T, b: T): Int =
${compareEntitiesImpl('a, 'b)}
def compareEntitiesImpl[T: Type](a: Expr[T], b: Expr[T])(using Quotes): Expr[Int] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
val fields = tpe.typeSymbol.caseFields
val comparisons = fields.map { field =>
val fieldName = field.name
val aField = Select(a.asTerm, field)
val bField = Select(b.asTerm, field)
'{
val aVal = ${aField.asExpr}
val bVal = ${bField.asExpr}
if (aVal != null && bVal != null && aVal == bVal) 1 else 0
}
}
comparisons.reduceLeft((acc, next) => '{ $acc + $next })
}
@main def main(): Unit = {
val testCases = List(
(("USA", null, "New York"), ("USA", "NY", null)), // should count 1
((null, null, null), (null, null, null)), // should count 0
(("USA", null, null), ("USA", null, null)), // should count 1
(("Germany", "Berlin", "Berlin"), ("Germany", "Berlin", null)), // should count 2
(("France", "Paris", "Lyon"), ("France", "Paris", "Lyon")), // should count 3
(("Canada", null, "Toronto"), ("Canada", "Ontario", "Toronto")), // should count 2
((null, "California", null), (null, "California", "Los Angeles")), // should count 1
(("Italy", "Rome", null), ("Italy", null, "Rome")), // should count 1
(("Japan", "Tokyo", "Shibuya"), ("Japan", "Kyoto", "Osaka")), // should count 1
(("Poland", null, "New York"), ("USA", "NY", null)), // should count 0
)
testCases.foreach { case (a, b) =>
val addrA = Address(a._1, a._2, a._3)
val addrB = Address(b._1, b._2, b._3)
println(compareEntity(addrA, addrB))
}
}
case class Address(country: String, state: String, city: String)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment