Skip to content

Instantly share code, notes, and snippets.

@calvinlfer
Last active March 13, 2022 15:42
Show Gist options
  • Save calvinlfer/cb38523e4536ecda1b6cf14ee3b03ff5 to your computer and use it in GitHub Desktop.
Save calvinlfer/cb38523e4536ecda1b6cf14ee3b03ff5 to your computer and use it in GitHub Desktop.
This shows how to do a Diff + Merge for any datatype provided you have the appropriate Diff and Merge all the primitive instances used by the datatype. This is done using Shapeless 2.3.3. Diff and Merge go hand-in-hand
// Unhappy with the default behavior of a particular datatype?
// You can override it as you wish, you must write a Diff and merge for the datatype you wish to override
final case class Address(
streetNumber: Int,
streetName: String,
city: String,
country: String
)
// We want to override Address in a way that we the user deem fit
final case class AddressDiff(
streetNumber: Diff[Int],
streetName: Diff[String],
city: Diff[String],
country: Diff[String]
)
implicit val deltaOptAddress: Delta.Aux[Option[Address], Option[AddressDiff]] =
new Delta[Option[Address]] {
override type Out = Option[AddressDiff]
override def apply(existing: Option[Address], incoming: Option[Address]): Out =
(existing, incoming) match {
case (Some(Address(eSNum, eSNam, eCity, eCountry)), Some(Address(iSNum, iSNam, iCity, iCountry))) =>
Option(
AddressDiff(
intDelta(eSNum, iSNum),
stringDelta(eSNam, iSNam),
stringDelta(eCity, iCity),
stringDelta(
eCountry,
iCountry
)
)
)
case (None, Some(Address(iSNum, iSNam, iCity, iCountry))) =>
Option(
AddressDiff(Diff.Change(iSNum), Diff.Change(iSNam), Diff.Change(iCity), Diff.Change(iCountry))
)
case (Some(Address(_, _, _, _)), None) =>
None
case (None, None) => None
}
}
implicit val mergeOptAddress: Merge.Aux[Option[Address], Option[AddressDiff]] =
new Merge[Option[Address]] {
override type D = Option[AddressDiff]
override def apply(base: Option[Address], diff: D): Option[Address] = (base, diff) match {
case (Some(existing), Some(diff)) =>
Some(
existing
.copy(
streetNumber = diff.streetNumber.getOrElse(existing.streetNumber),
streetName = diff.streetName.getOrElse(existing.streetName),
city = diff.city.getOrElse(existing.city),
country = diff.country.getOrElse(existing.country)
)
)
case (Some(_), None) => None
case (
None,
Some(
AddressDiff(Diff.Change(streetNum), Diff.Change(streetName), Diff.Change(city), Diff.Change(country))
)
) =>
Some(Address(streetNum, streetName, city, country))
case (None, _) => None
}
}
import shapeless._
import java.time._
sealed trait Diff[+A] { self =>
def getOrElse[A0 >: A](default: A0): A0 = self match {
case Diff.Identical => default
case Diff.Change(value) => value
}
}
object Diff {
case object Identical extends Diff[Nothing]
final case class Change[A](value: A) extends Diff[A]
def change[A](value: A): Diff[A] = Change(value)
}
trait Delta[In] {
type Out
def apply(existing: In, incoming: In): Out
}
object Delta extends DeltaLowPriority {
type Aux[I, O] = Delta[I] {
type Out = O
}
def apply[A](implicit proof: Lazy[Delta[A]]): Delta.Aux[A, proof.value.Out] = proof.value
private def instance[A](same: (A, A) => Boolean): Delta.Aux[A, Diff[A]] = new Delta[A] {
type Out = Diff[A]
override def apply(existing: A, incoming: A): Diff[A] =
if (same(existing, incoming)) Diff.Identical
else Diff.Change(incoming)
}
implicit val booleanDelta: Aux[Boolean, Diff[Boolean]] = instance[Boolean](_ == _)
implicit val stringDelta: Aux[String, Diff[String]] = instance[String](_ == _)
implicit val intDelta: Aux[Int, Diff[Int]] = instance[Int](_ == _)
implicit val localDateDelta: Aux[LocalDate, Diff[LocalDate]] = instance[LocalDate](_ isEqual _)
implicit val localDateTimeDelta: Aux[LocalDateTime, Diff[LocalDateTime]] = instance[LocalDateTime](_ isEqual _)
implicit def listDelta[A](implicit
diffProof: Aux[A, Diff[A]],
orderProof: Ordering[A]
): Aux[List[A], Diff[List[A]]] =
instance[List[A]] { (incoming, existing) =>
val iS = incoming.sorted
val eS = existing.sorted
iS == eS
}
}
// Implicit prioritization tricks are used to allow the user to override datatypes
trait DeltaLowPriority {
// We keep these derivations in low priority so if you provide more specific proof, it will prefer that over these
implicit val hnilDelta: Delta.Aux[HNil, HNil] = new Delta[HNil] {
// This must be HNil or the HList will not terminate and the code will not compile
type Out = HNil
override def apply(existing: HNil, incoming: HNil): HNil = existing
}
implicit def optionDeltaHList[A <: HList](implicit
deltaA: Lazy[Delta[A]]
): Aux[Option[A], Option[deltaA.value.Out]] =
new Delta[Option[A]] {
override type Out = Option[deltaA.value.Out]
override def apply(existing: Option[A], incoming: Option[A]): Out = (existing, incoming) match {
case (Some(e), Some(i)) => Option(deltaA.value.apply(e, i))
case (_, _) => None
}
}
implicit def optionDelta[A](implicit proofNotHList: A =:!= HList): Aux[Option[A], Diff[Option[A]]] =
new Delta[Option[A]] {
type Out = Diff[Option[A]]
override def apply(existing: Option[A], incoming: Option[A]): Diff[Option[A]] =
if (existing != incoming) Diff.Change(incoming)
else Diff.Identical
}
implicit def hconsDelta[H, T <: HList](implicit
proofHead: Lazy[Delta[H]],
proofTail: Lazy[Delta[T] { type Out <: HList }]
): Delta.Aux[H :: T, proofHead.value.Out :: proofTail.value.Out] = new Delta[H :: T] {
override type Out = proofHead.value.Out :: proofTail.value.Out
override def apply(existing: H :: T, incoming: H :: T): proofHead.value.Out :: proofTail.value.Out = {
val diffHead = proofHead.value.apply(existing.head, incoming.head)
val diffTail = proofTail.value.apply(existing.tail, incoming.tail)
diffHead :: diffTail
}
}
implicit def genericDelta[A, Repr, Out](implicit
gen: Generic.Aux[A, Repr],
delta: Lazy[Delta.Aux[Repr, Out]]
): Delta.Aux[A, Out] =
new Delta[A] {
override type Out = delta.value.Out
override def apply(existing: A, incoming: A): Out =
delta.value.apply(gen.to(existing), gen.to(incoming))
}
}
object Example {
final case class Person(
firstName: String,
lastName: String,
age: Int,
address: Option[Address],
optionalInfo: Option[Int],
address2: Address
)
val deltaPerson = Delta[Person]
val mergePerson = Merge[Person]
val p1 = Person(
"Calvin",
"Fernandes",
29,
Option(Address(73, "Vessel Crescent", "Scarborough", "Canada")),
None,
Address(73, "Vessel Crescent", "Scarborough", "Canada")
)
val p1_1 = p1.copy(optionalInfo = Some(10))
val p2 = Person(
"Calvin",
"Fernandes",
30,
Option(Address(75, "Vessel Crescent", "Scarborough", "Canada")),
None,
Address(75, "Vessel Crescent", "Toronto", "Canada")
)
val personDeltaInstanceP1ToP11 = deltaPerson(p1, p1_1)
val personDeltaInstanceP1toP2 = deltaPerson(p1, p2)
val step1 = mergePerson(p1, personDeltaInstanceP1ToP11)
val step2 = mergePerson(step1, personDeltaInstanceP1toP2)
}
import shapeless._
import java.time._
trait Merge[A] { self =>
type D
def apply(base: A, diff: D): A
}
object Merge extends MergeLowPriorityInstance {
type Aux[A, Diff] = Merge[A] {
type D = Diff
}
def apply[A](implicit proof: Lazy[Merge[A]]): Merge.Aux[A, proof.value.D] = proof.value
private def instance[A]: Merge.Aux[A, Diff[A]] = new Merge[A] {
type D = Diff[A]
override def apply(base: A, diff: D): A = diff match {
case Diff.Identical => base
case Diff.Change(value) => value
}
}
implicit val booleanMerge: Merge.Aux[Boolean, Diff[Boolean]] = instance[Boolean]
implicit val stringMerge: Merge.Aux[String, Diff[String]] = instance[String]
implicit val intMerge: Merge.Aux[Int, Diff[Int]] = instance[Int]
implicit val localDateMerge: Merge.Aux[LocalDate, Diff[LocalDate]] = instance[LocalDate]
implicit val localDateTimeMerge: Merge.Aux[LocalDateTime, Diff[LocalDateTime]] = instance[LocalDateTime]
}
// Implicit prioritization tricks are used to allow the user to override datatypes
trait MergeLowPriorityInstance {
implicit def optionMerge[A](implicit proofNotHList: A =:!= HList): Merge.Aux[Option[A], Diff[Option[A]]] =
new Merge[Option[A]] {
type D = Diff[Option[A]]
override def apply(base: Option[A], diff: Diff[Option[A]]): Option[A] = diff match {
case Diff.Identical => base
case Diff.Change(value) => value
}
}
implicit def optionMergeHList[A <: HList](implicit
mergeA: Lazy[Merge[A]]
): Merge.Aux[Option[A], Option[mergeA.value.D]] =
new Merge[Option[A]] {
override type D = Option[mergeA.value.D]
override def apply(base: Option[A], diff: D): Option[A] =
(base, diff) match {
case (Some(b), Some(d)) => Some(mergeA.value.apply(b, d))
case (Some(b), None) => Some(b)
case (None, None) => None
}
}
implicit val hnilMerge: Merge.Aux[HNil, HNil] = new Merge[HNil] {
type D = HNil
override def apply(base: HNil, diff: D): HNil = HNil
}
implicit def hconsMerge[H, T <: HList](implicit
proofHead: Lazy[Merge[H]],
proofTail: Lazy[Merge[T] { type D <: HList }]
): Merge.Aux[H :: T, proofHead.value.D :: proofTail.value.D] = new Merge[H :: T] {
type D = proofHead.value.D :: proofTail.value.D
override def apply(base: H :: T, diff: D): H :: T = {
val resHead: H = proofHead.value.apply(base.head, diff.head)
val resTail: T = proofTail.value.apply(base.tail, diff.tail)
resHead :: resTail
}
}
implicit def genericMerge[A, Repr](implicit
gen: Generic.Aux[A, Repr],
merge: Lazy[Merge[Repr]]
): Merge.Aux[A, merge.value.D] =
new Merge[A] {
override type D = merge.value.D
override def apply(base: A, diff: D): A = {
val repr: Repr = merge.value.apply(gen.to(base), diff)
gen.from(repr)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment