Skip to content

Instantly share code, notes, and snippets.

@andyscott
Last active September 18, 2016 16:17
Show Gist options
  • Save andyscott/428f84db8b2665d29c795ffbeca2e590 to your computer and use it in GitHub Desktop.
Save andyscott/428f84db8b2665d29c795ffbeca2e590 to your computer and use it in GitHub Desktop.
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