Last active
September 18, 2016 16:17
-
-
Save andyscott/428f84db8b2665d29c795ffbeca2e590 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import cats._ | |
import cats.data.OptionT | |
import cats.free._ | |
import cats.syntax.option._ | |
import shapeless.{ Id ⇒ _, _ } | |
import shapeless.labelled.{ field, FieldType } | |
//import shapeless.ops.record._ | |
import shapeless.syntax.RecordOps | |
trait AllNull[R <: HList] { | |
def apply(): R | |
} | |
// really this is a reference instance generator | |
object AllNull extends AllNullLowPrio { | |
implicit final val hnilAllNull: AllNull[HNil] = | |
new AllNull[HNil] { | |
final def apply(): HNil = HNil | |
} | |
// eeh... primitives can't be null | |
implicit final def hconsAllNullInt[K <: Symbol, T <: HList](implicit tailAllNull: AllNull[T]): AllNull[FieldType[K, Int] :: T] = | |
new AllNull[FieldType[K, Int] :: T] { | |
final def apply(): FieldType[K, Int] :: T = | |
field[K](Int.MinValue) :: tailAllNull() | |
} | |
class AllNullPartiallyApplied[A] { | |
def apply[R <: HList]()(implicit | |
gen: LabelledGeneric.Aux[A, R], | |
nuller: AllNull[R]): A = gen.from(nuller()) | |
} | |
def allNull[A] = new AllNullPartiallyApplied[A] | |
} | |
sealed trait AllNullLowPrio { | |
implicit final def hconsAllNull[K <: Symbol, V, T <: HList](implicit tailAllNull: AllNull[T]): AllNull[FieldType[K, V] :: T] = | |
new AllNull[FieldType[K, V] :: T] { | |
final def apply(): FieldType[K, V] :: T = | |
field[K](null.asInstanceOf[V]) :: tailAllNull() | |
} | |
} | |
trait Differ[R <: HList] { | |
type Out <: HList | |
def apply(p: R, c: R): Out | |
} | |
object Differ { | |
final type Aux[R <: HList, Out0 <: HList] = Differ[R] { type Out = Out0 } | |
implicit final val hnilDiffer: Aux[HNil, HNil] = | |
new Differ[HNil] { | |
final type Out = HNil | |
final def apply(p: HNil, c: HNil): HNil = HNil | |
} | |
implicit final def hconsDiffer[K <: Symbol, V, T <: HList](implicit tailDiffer: Differ[T]): Aux[FieldType[K, V] :: T, FieldType[K, Option[V]] :: tailDiffer.Out] = | |
new Differ[FieldType[K, V] :: T] { | |
final type Out = FieldType[K, Option[V]] :: tailDiffer.Out | |
final def apply( | |
p: FieldType[K, V] :: T, | |
c: FieldType[K, V] :: T | |
): FieldType[K, Option[V]] :: tailDiffer.Out = | |
field[K](if (c.head != p.head) Some(c.head) else None) :: tailDiffer(p.tail, c.tail) | |
} | |
/** Calculates the diff between two instances of a given case class */ | |
def diff[A, R <: HList, O <: HList](previous: A, current: A)(implicit | |
gen: LabelledGeneric.Aux[A, R], | |
differ: Differ.Aux[R, O]): differ.Out = differ(gen.to(previous), gen.to(current)) | |
/** Decodes updates applied by transformation `update` from `A ⇒ A` */ | |
def decode[A, R <: HList, O <: HList](update: A ⇒ A)(implicit | |
gen: LabelledGeneric.Aux[A, R], | |
differ: Differ.Aux[R, O], | |
allNull: AllNull[R]): differ.Out = { | |
val empty = allNull() | |
differ(empty, gen.to(update(gen.from(empty)))) | |
} | |
} | |
trait Patcher[T, M] { | |
def apply(data: T, mods: M): T | |
} | |
object Patcher { | |
def apply[A, B](implicit ev: Patcher[A, B]): Patcher[A, B] = ev | |
implicit final val hnilPatcher: Patcher[HNil, HNil] = | |
new Patcher[HNil, HNil] { | |
final def apply(l: HNil, m: HNil): HNil = HNil | |
} | |
implicit final def hconsPatcher[K <: Symbol, V, L <: HList, M <: HList]( | |
implicit | |
tailPatcher: Lazy[Patcher[L, M]] | |
): Patcher[FieldType[K, V] :: L, FieldType[K, Option[V]] :: M] = | |
new Patcher[FieldType[K, V] :: L, FieldType[K, Option[V]] :: M] { | |
final def apply( | |
l: FieldType[K, V] :: L, | |
m: FieldType[K, Option[V]] :: M): FieldType[K, V] :: L = | |
field[K](m.head.getOrElse(l.head)) :: tailPatcher.value(l.tail, m.tail) | |
} | |
implicit final def hconsPatcherSkipLeft[K <: Symbol, V, L <: HList, M <: HList]( | |
implicit | |
tailPatcher: Lazy[Patcher[L, M]] | |
): Patcher[FieldType[K, V] :: L, M] = | |
new Patcher[FieldType[K, V] :: L, M] { | |
final def apply( | |
l: FieldType[K, V] :: L, | |
m: M): FieldType[K, V] :: L = | |
l.head :: tailPatcher.value(l.tail, m) | |
} | |
implicit final def genericPatcher[A, LA <: HList, B, LB <: HList]( | |
implicit | |
genA: LabelledGeneric.Aux[A, LA], | |
genB: LabelledGeneric.Aux[B, LB], | |
patcher: Patcher[LA, LB] | |
): Patcher[A, B] = new Patcher[A, B] { | |
def apply(data: A, mods: B): A = | |
genA from patcher(genA.to(data), genB.to(mods)) | |
} | |
} | |
// record | |
case class Foo( | |
id: String, | |
a: String, | |
b: Int, | |
c: Option[Double] | |
) | |
case class FooPatch( | |
//id: Option[String], | |
a: Option[String] = None, | |
b: Option[Int] = None, | |
c: Option[Option[Double]] = None | |
) | |
object FooPatch { | |
val patch: Patcher[Foo, FooPatch] = Patcher[Foo, FooPatch] | |
} | |
object Foo { | |
type Create = String ⇒ Foo | |
type Update = Foo ⇒ Foo | |
} | |
// smart constructors | |
object FooOps { | |
type IO[A] = Free[FooOp, A] | |
def createFoo(a: String, b: Int, c: Option[Double]): IO[Option[Foo]] = createFoo(id ⇒ Foo(id, a, b, c)) | |
def createFoo(create: Foo.Create): IO[Option[Foo]] = Free.liftF(FooOp.Create(create)) | |
def readFoo(id: String): IO[Option[Foo]] = Free.liftF(FooOp.Read(id)) | |
def updateFoo( | |
id: String, | |
a: Option[String] = None, | |
b: Option[Int] = None, | |
c: Option[Option[Double]] = None | |
): IO[Option[Foo]] = updateFoo(id, source ⇒ source.copy( | |
a = a getOrElse source.a, | |
b = b getOrElse source.b, | |
c = c getOrElse source.c | |
)) | |
// is there any way to ensure that updates doesn't modify the id field? | |
def updateFoo(id: String, update: Foo.Update): IO[Option[Foo]] = Free.liftF(FooOp.Update(id, update)) | |
} | |
// algebra | |
sealed trait FooOp[A] | |
object FooOp { | |
case class Create(create: Foo.Create) extends FooOp[Option[Foo]] | |
case class Read(id: String) extends FooOp[Option[Foo]] | |
case class Update(id: String, update: Foo.Update) extends FooOp[Option[Foo]] | |
} | |
// interpreter | |
class FooOpInterpreter() extends (FooOp ~> Id) { | |
var foos: Map[String, Foo] = Map.empty | |
def apply[A](op: FooOp[A]): Id[A] = op match { | |
case FooOp.Create(create) ⇒ | |
val id = s"foo${System.currentTimeMillis}" | |
foos.get(id) match { | |
case Some(existing) ⇒ None | |
case None ⇒ | |
val foo = create(id) | |
// TODO: do we need to check that id was set correctly? | |
foos = foos + (id → foo) | |
Some(foo) | |
} | |
case FooOp.Read(id) ⇒ | |
foos.get(id) | |
case FooOp.Update(id, update) ⇒ | |
val changes = Differ.decode(update) | |
val changesMap = new RecordOps(changes).toMap | |
println("Semi accurate changes: " + changesMap) | |
foos.get(id) match { | |
case Some(existing) ⇒ | |
val updated = update(existing) | |
foos = foos + (updated.id → updated) | |
Some(updated) | |
case None ⇒ None | |
} | |
} | |
} | |
// dummy app | |
object FooApp { | |
def main(args: Array[String]): Unit = { | |
// patch testing | |
val foo = Foo("foo1", "hello", 2, None) | |
val patch = FooPatch(a = "world".some) | |
val res1 = FooPatch.patch(foo, patch) | |
println(foo) | |
println(patch) | |
println(res1) | |
// diff testing | |
val program = for { | |
foo1 ← OptionT(FooOps.createFoo(a = "hello", b = 1, c = Some(2.2))) | |
foo2 ← OptionT(FooOps.updateFoo(foo1.id, a = "world".some, c = None.some)) | |
} yield () | |
val interpreter = new FooOpInterpreter() | |
val res = program.value.foldMap(interpreter) | |
println("res " + res) | |
println("foos " + interpreter.foos) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment