/patchless

Patch data type for Scala and shapeless

Primary LanguageScalaApache License 2.0Apache-2.0

patchless

Build Status Maven Central

patchless is a tiny Scala library which provides:

  • A data type Patch[T], which extends T => T and encapsulates a set of updates to be performed to values of type T.
  • A typeclass Patchable[T], which supports the data type above.

It uses shapeless to derive Patchable[T] for any case class.

Dependency

Patchless is published to Maven Central – put this in your build.sbt:

libraryDependencies += "io.github.jeremyrsmith" %% "patchless" % "1.0.4"

Usage

The core of patchless provides only two simple way to create a Patch[T] for any given T:

  • The apply syntax (macro-driven):
import patchless.Patch
case class Foo(a: String, b: Int, c: Boolean)
val patch = Patch[Foo](b = 22)
  • The Patch.diff[T] static method:
case class Foo(a: String, b: Int, c: Boolean)
val a = Foo("test", 22, true)
val b = Foo("patched", 22, true)
val patch = Patch.diff(a, b)
patch(a) // Foo("patched", 22, true)
patch(Foo("wibble", 44, false)) // Foo("patched", 44, false)

Additionally, the patchless-circe module provides decoders directly from JSON to Patch[T]. See below for details.

Using the patch fields

The primary advantage of Patch[T] over simply T => T is that the updated fields can be accessed as a shapeless Record of Options. Each field retains the name from the original case class T, but its value type is lifted to an Option of the original type within the case class.

The Record is accessible in two ways. The first is simply by the updates member of the Patch value:

println(patch) // Some("patched") :: None :: None :: HNil

This alone doesn't turn out to be all that useful from a typelevel standpoint - Scala doesn't inherently know the type of the updates field, so your options there are limited.

So patchless does some additional type voodoo to allow you to recover a statically known Record for a Patch[T] of a concrete, statically known type T. This is done with the implicit enrichment method patchUpdates, which allows you to do typelevel things like mapping over the updates HList or summoning typeclasses for it:

object mapUpdates extends Poly1 {
  implicit def cases[K <: Symbol, T](implicit
    name: Witness.Aux[K]
  ) = at[FieldType[K, T]] {
    field => name.value.name -> field.toString
  }
}
patch.patchUpdates.map(mapUpdates).toList
// List(("a", "Some(patched)"), ("b", "None"), ("c", "None")) 

Please note that this only works for a concrete T. If T is abstract (such as in a polymorphic method over Patch types) then you'll still have to parameterize over various HList types and require various implicit shapeless Aux typeclasses over them as usual – starting with Patchable.Aux[T, U] where U will be inferred to the type of the Updates record for T.

def doPatchyStuff[T, U <: HList, A <: HList](patch: Patch[T])(implicit
  patchable: Patchable.Aux[T, U],
  liftAll: LiftAll.Aux[MyTC, U, A],
  toList: ToList[A, Any]
) = ???

Also, be aware that patchUpdates involves a typecast; it's assumed that the Updates of the Patch[T] value has the same type as the Patchable[T] that is in implicit scope. This is usually a safe assumption, but it's not guaranteed to be safe. In an effort to make it as close as possible to a guarantee, Patchable is defined as sealed, which means that only the blessed derivations can ever be used to create it; these ought to be deterministic for a particular T, but Scala provides no way to express this and thus a typecast is still necessary.

patchless-circe

Derived decoders and encoders are provided in the patchless-circe module.

In build.sbt:

libraryDependencies += "io.github.jeremyrsmith" %% "patchless-circe" % "1.0.2"

There are two different imports, depending on how you're using circe. You need to have at least circe-generic, and you can also optionally use circe-generic-extras (which is marked as a provided dependency in case you don't use it).

You also need to be using automatic derivation for this to be of any use; it's not possible to derive a Patch[T] decoder for a semiauto or manual decoder of T.

For vanilla automatic derivation:

import io.circe.generic.auto._
import patchless.circe._
import cats.syntax.either._ // for working with results

case class Foo(aString: String, bInt: Int)
val parsed = io.circe.parser.parse("""{"aString": "patched"}""")
parsed.valueOr(throw _).as[Patch[Foo]].valueOr(throw _)
parsed.updates          // Some("patched") :: None :: HNil
parsed(Foo("blah", 22)) // Foo("patched", 22)

Configurable derivation is the same, but import patchless.circe.extras._ instead; your implicit Configuration will be used to derive the decoders for Patch types.

Encoders work the same way, but be aware that the JSON output depends on the printer used – in particular, you'll typically want to dropNullKeys if you're outputting Patch values to JSON.

License

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Code of Conduct

The patchless project supports the Typelevel Code of Conduct and wants all its channels to be welcoming environments for everyone.