Skip to content

Instantly share code, notes, and snippets.

@ShahOdin
Last active December 6, 2019 14:30
Show Gist options
  • Save ShahOdin/b63bf3a322f0806305f6fd0bf165ee82 to your computer and use it in GitHub Desktop.
Save ShahOdin/b63bf3a322f0806305f6fd0bf165ee82 to your computer and use it in GitHub Desktop.
reconstructing state
package com.itv.sif.db
import cats.Show
import cats.data.{NonEmptyList, NonEmptySet, Validated}
import cats.syntax.apply._
import cats.syntax.either._
import cats.syntax.validated._
import cats.syntax.show._
import cats.instances.all._
import cats.kernel.Semigroup
//This demo showcases a system with two entities that can be independently updated.
//we can either aggregate them dynamically (in-memory) and reconstruct a history of the
//evolution of the aggregate model.
//alternatively, we can record the aggregate model and update its value at every update so that one can
//read a static list of values for the aggregate model.
//Or we can do both, as the two give us different set of information and can be different if
//our pipeline isn't updating the aggregate model correctly.
object CoolDemo extends App {
sealed trait Color
object Color {
case object Green extends Color
case object Red extends Color
}
sealed trait Hobby
object Hobby {
case object BirdWatching extends Hobby
case object Sports extends Hobby
}
type TimeCount = Long
case class Failed(reasons: NonEmptyList[String], failedAt: TimeCount, sources: NonEmptySet[String])
object Failed{
def one(reason: String, failureTime: TimeCount, sources: NonEmptySet[String]) = Failed(NonEmptyList.one(reason), failureTime, sources)
implicit val semiGroup: Semigroup[Failed] = (x: Failed, y: Failed) => Failed(x.reasons ::: y.reasons, math.max(x.failedAt, y.failedAt), (x.sources ++ y.sources))
}
type ValueOrWhyNot[A] = Either[Failed, A]
case class Profile(favoriteColor: Color, wantsKids: Boolean, favoriteHobby: Hobby)
object Profile {
def fromPersonRecord(personRecord: PersonRecord, attemptTime: TimeCount): Validated[Failed, Profile] = {
(
personRecord.favoriteColor.toValidated,
personRecord.wantsKids.toValidated,
personRecord.favoriteHobby.toValidated
).mapN{
case (c, wk, h) => Profile(c, wk, h)
}.leftMap(
_.copy(failedAt = attemptTime)
)
}
}
case class PersonRecord(favoriteColor: ValueOrWhyNot[Color], wantsKids: ValueOrWhyNot[Boolean], favoriteHobby: ValueOrWhyNot[Hobby], updateTime: TimeCount)
case class Match(p1: Profile, p2: Profile, matchedAt: TimeCount)
object Match {
def fromProfiles(p1: Profile, p2: Profile, matchAttemptTime: TimeCount): Validated[Failed, Match] = {
if (matchScore(p1, p2) >= 2) {
Match(p1, p2, matchAttemptTime).valid
} else {
Failed.one("Profiles complete but the two do not have enough in common.", matchAttemptTime, NonEmptySet.of("Alice", "Bob")).invalid
}
}
def matchScore(p1: Profile, p2: Profile): Int = {
implicit class convertBooleanToInt(b: Boolean) {
def toInt = if(b) 1 else 0
}
NonEmptyList.of(
p1.wantsKids == p2.wantsKids,
p1.favoriteColor == p2.favoriteColor,
p1.favoriteHobby == p2.favoriteHobby
).map(_.toInt)
.reduce
}
}
var alice: NonEmptyList[PersonRecord] = NonEmptyList.one(
PersonRecord(
favoriteColor = Failed.one("Alice Color: N/A", failureTime = 0, NonEmptySet.one("Alice")).asLeft,
wantsKids = Failed.one("Alice Wants Kids: N/A", failureTime = 0, NonEmptySet.one("Alice")).asLeft,
favoriteHobby = Failed.one("Alice Hobby: N/A", failureTime = 0, NonEmptySet.one("Alice")).asLeft,
updateTime = 0
)
)
var bob: NonEmptyList[PersonRecord] = NonEmptyList.one(
PersonRecord(
favoriteColor = Failed.one("Bob Color: N/A", failureTime = 0, NonEmptySet.one("Bob")).asLeft,
wantsKids = Failed.one("Bob Wants Kids: N/A", failureTime = 0, NonEmptySet.one("Bob")).asLeft,
favoriteHobby = Failed.one("Bob Hobby: N/A", failureTime = 0, NonEmptySet.one("Bob")).asLeft,
updateTime = 0
)
)
var matchHistory: List[Either[Failed, Match]] = Nil
def updateMatchStatus(timeCount: TimeCount): Unit = {
val latestMatchStatus = matchRecords(
alice.head,
bob.head,
timeCount
)
.andThen( _ => Failed.one(reason = "Computer says no.", timeCount, NonEmptySet.of("Bob", "Alice")).invalid[Match])
.toEither
matchHistory = latestMatchStatus :: matchHistory
}
def updateAliceState(record: PersonRecord): Unit = {
alice = record :: alice
updateMatchStatus(record.updateTime)
}
def updateBobState(record: PersonRecord): Unit = {
bob = record :: bob
updateMatchStatus(record.updateTime)
}
def updateAliceHobby(maybeHobby: ValueOrWhyNot[Hobby], count: TimeCount): Unit = {
updateAliceState(alice.head.copy(favoriteHobby = maybeHobby, updateTime = count))
}
def updateAliceColor(maybeColor: ValueOrWhyNot[Color], count: TimeCount): Unit = {
updateAliceState(alice.head.copy(favoriteColor = maybeColor, updateTime = count))
}
def updateAliceWantsKids(maybeWantsKids: ValueOrWhyNot[Boolean], count: TimeCount): Unit = {
updateAliceState(alice.head.copy(wantsKids = maybeWantsKids, updateTime = count))
}
def updateBobHobby(maybeHobby: ValueOrWhyNot[Hobby], count: TimeCount): Unit = {
updateBobState(bob.head.copy(favoriteHobby = maybeHobby, updateTime = count))
}
def updateBobColor(maybeColor: ValueOrWhyNot[Color], count: TimeCount): Unit = {
updateBobState(bob.head.copy(favoriteColor = maybeColor, updateTime = count))
}
def updateBobWantsKids(maybeWantsKids: ValueOrWhyNot[Boolean], count: TimeCount): Unit = {
updateBobState(bob.head.copy(wantsKids = maybeWantsKids, updateTime = count))
}
def aliceRecordAtTime(count: TimeCount): PersonRecord = alice
.filter(_.updateTime <= count)
.maxBy(_.updateTime)
def bobRecordAtTime(count: TimeCount): PersonRecord = bob
.filter(_.updateTime <= count)
.maxBy(_.updateTime)
def updateEntryTimes: NonEmptyList[TimeCount] = {
alice.map(_.updateTime) ::: bob.map(_.updateTime)
}.distinct
def aliceAndBobRecords: NonEmptyList[(PersonRecord, PersonRecord, TimeCount)] = updateEntryTimes.map { t =>
(
aliceRecordAtTime(t),
bobRecordAtTime(t),
t
)
}
def matchRecords(record1: PersonRecord, record2: PersonRecord, timeCount: TimeCount): Validated[Failed, Match] = (
Profile.fromPersonRecord(record1, timeCount),
Profile.fromPersonRecord(record2, timeCount)
).mapN {
case (profile1, profile2) =>
Match.fromProfiles(profile1, profile2, timeCount)
}.andThen(identity)
def recordMatchability(nel: NonEmptyList[PersonRecord]): NonEmptyList[(Boolean, TimeCount)] = nel.map{
r =>
val ready = r.favoriteColor.isRight && r.favoriteHobby.isRight && r.wantsKids.isRight
(ready, r.updateTime)
}
case class MatchResult(value: Either[Failed, Match]) extends AnyVal
object MatchResult {
implicit val show: Show[MatchResult] = {
matchResult =>
matchResult.value match {
case Left(failed) => s"Failed because of: ${failed.sources.toNonEmptyList.toList.mkString(",")}, detailed: ${failed.reasons.toList.mkString(",")}, updated at: ${failed.failedAt}"
case Right(matched) => matched.toString
}
}
}
def reconstructedAliceAndBobCompatibilityOverTime: NonEmptyList[MatchResult] = {
aliceAndBobRecords.map {
case (aliceRecord, bobRecord, t) =>
matchRecords(aliceRecord, bobRecord, t).toEither
}.map(
MatchResult.apply
).sortBy(
_.value
.map(_.matchedAt)
.valueOr(_.failedAt)
)
}
def recordedAliceAndBobCompatibilityOverTime: List[MatchResult] = matchHistory
.map(MatchResult.apply)
.sortBy(
_.value
.map(_.matchedAt)
.valueOr(_.failedAt)
)
updateMatchStatus(0)
updateAliceHobby(Hobby.BirdWatching.asRight, 1)
updateBobHobby(Hobby.Sports.asRight, 2)
updateAliceColor(Color.Red.asRight, 3)
updateAliceWantsKids(false.asRight, 4)
updateBobWantsKids(true.asRight, 6)
updateBobColor(Color.Red.asRight, 7)
updateBobWantsKids(false.asRight, 9)
updateAliceWantsKids(Failed.one("Alice wants kids: It's complicated", 12, NonEmptySet.one("Alice")).asLeft, 11)
updateAliceWantsKids(true.asRight, 12)
println("\n***recorded info over time. shows the bug at the outermost level: `computer says no` ***\n")
recordedAliceAndBobCompatibilityOverTime.map(r => println(r.show))
println("\n***alice ready to be matched over time***\n")
recordMatchability(alice).sortBy(_._2).map(println)
println("\n***bob ready to be matched over time***\n")
recordMatchability(bob).sortBy(_._2).map(println)
println("\n***reconstructed info over time. ***\n")
reconstructedAliceAndBobCompatibilityOverTime.map(r => println(r.show))
}
//
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Alice Hobby: N/A,Bob Color: N/A,Bob Wants Kids: N/A,Bob Hobby: N/A, updated at: 0
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A,Bob Hobby: N/A, updated at: 1
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A, updated at: 2
//Failed because of: Alice,Bob, detailed: Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A, updated at: 3
//Failed because of: Bob, detailed: Bob Color: N/A,Bob Wants Kids: N/A, updated at: 4
//Failed because of: Bob, detailed: Bob Color: N/A, updated at: 6
//Failed because of: Alice,Bob, detailed: Profiles complete but the two do not have enough in common., updated at: 7
//Failed because of: Alice,Bob, detailed: Computer says no., updated at: 9
//Failed because of: Alice, detailed: Alice wants kids: It's complicated, updated at: 11
//Failed because of: Alice,Bob, detailed: Profiles complete but the two do not have enough in common., updated at: 12
//
//***alice ready to be matched over time***
//
//(false,0)
//(false,1)
//(false,3)
//(true,4)
//(false,11)
//(true,12)
//
//***bob ready to be matched over time***
//
//(false,0)
//(false,2)
//(false,6)
//(true,7)
//(true,9)
//
//***reconstructed info over time. ***
//
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Alice Hobby: N/A,Bob Color: N/A,Bob Wants Kids: N/A,Bob Hobby: N/A, updated at: 0
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A,Bob Hobby: N/A, updated at: 1
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A, updated at: 2
//Failed because of: Alice,Bob, detailed: Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A, updated at: 3
//Failed because of: Bob, detailed: Bob Color: N/A,Bob Wants Kids: N/A, updated at: 4
//Failed because of: Bob, detailed: Bob Color: N/A, updated at: 6
//Failed because of: Alice,Bob, detailed: Profiles complete but the two do not have enough in common., updated at: 7
//Match(Profile(Red,false,BirdWatching),Profile(Red,false,Sports),9)
//Failed because of: Alice, detailed: Alice wants kids: It's complicated, updated at: 11
//Failed because of: Alice,Bob, detailed: Profiles complete but the two do not have enough in common., updated at: 12
@ShahOdin
Copy link
Author

ShahOdin commented Dec 6, 2019

added source of failure to make it easier to read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment