patchless is a tiny Scala library which provides:
- A data type
Patch[T]
, which extendsT => T
and encapsulates a set of updates to be performed to values of typeT
. - A typeclass
Patchable[T]
, which supports the data type above.
It uses shapeless to derive Patchable[T]
for any case class.
Patchless is published to Maven Central – put this in your build.sbt:
libraryDependencies += "io.github.jeremyrsmith" %% "patchless" % "1.0.4"
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.
The primary advantage of Patch[T]
over simply T => T
is that the updated fields can be accessed as a shapeless
Record
of Option
s. 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.
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.
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.
The patchless project supports the Typelevel Code of Conduct and wants all its channels to be welcoming environments for everyone.