Deriving incomplete type class instances
Suppose we've got a simple representation of a user:
case class User(id: Long, name: String, email: String)
Now suppose we're writing a web service where we allow clients to post some
JSON to a resource to create a new user. We get to pick the id
, not the client,
so we might accept something like this:
{
"name": "Foo McBar",
"email": "foo@mcbar.com"
}
If we're using a type class-based JSON library like Argonaut, we'll
probably have written a codec instance for User
(or we may be using a library
like argonaut-shapeless that derives instances for our
case classes automatically).
The problem is that our User
codec won't work on JSON like the
example above (since it's missing the id
field).
One way around this issue would be to write a new case class that represents a partial user:
case class PartialUser(name: String, email: String)
Or we could use a framework like metarest that would handle this
boilerplate for us. It'd be nice to have a more generic solution, though. One
possibility would be to have partial DecodeJson
instances derived for us. In
our example above, this might look like the following:
import argonaut._, Argonaut._, Shapeless._
case class User(id: Long, name: String, email: String)
val json = """
{
"name": "Foo McBar",
"email": "foo@mcbar.com"
}
"""
Parse.decode[Long => User](json)
But this of course doesn't work, since argonaut-shapeless only generates
instances for things that have LabelledGeneric
instances.
It is possible to make this happen, though—we just need something like this:
import scalaz.Functor
import shapeless._
import shapeless.ops.function.FnFromProduct
trait Incompletes[C[_]] {
implicit def incompleteInstance[
F, // The `FunctionN` that we want an instance for.
P <: HList, // The patch (possibly unlabeled).
A, // The case class (or whatever) that we're targeting.
T <: HList, // The labeled representation of `A`.
R <: HList // The remaining fields.
](implicit
ffp: FnFromProduct.Aux[P => A, F],
gen: LabelledGeneric.Aux[A, T],
complement: Complement.Aux[T, P, R],
functor: Functor[C],
instance: C[R]
): C[F] = functor.map(instance)(r =>
ffp(p => gen.from(complement.insert(p, r)))
)
}
Note that we get all of this stuff off the shelf with Shapeless
(and Scalaz, which we're only using for its Functor
) except for
Complement
(more about that in a minute).
Now we can write something like this:
import argonaut.{ AutoDecodeJsons, AutoEncodeJsons, DecodeJson }
object ArgonautDerivation
extends AutoDecodeJsons with AutoEncodeJsons
with Incompletes[DecodeJson]
And then:
scala> case class User(id: Long, name: String, email: String)
defined class User
scala> Parse.decodeOption[Long => User](json).map(_(1001))
res0: Option[User] = Some(User(1001,Foo McBar,foo@mcbar.com))
Which is exactly what we wanted, with no boilerplate.
It also works with more complicated case classes (note the duplicate type):
case class User(id: Long, age: Long, name: String, email: String)
val lu = implicitly[DecodeJson[Long => User]]
Or with multiple missing pieces:
val lsu = implicitly[DecodeJson[(Long, String) => User]]
And if we really need to disambiguate missing fields with the same type, we can use labels:
import shapeless.labelled.{ FieldType, field }
val json = """
{
"id": 1001,
"name": "Foo McBar",
"email": "foo@mcbar.com"
}
"""
val withoutAge =
Parse.decodeOption[FieldType[Witness.`'age`.T, Long] => User](json)
And then:
scala> withoutAge.map(_(field(25)))
res1: Option[User] = Some(User(1001,25,Foo McBar,foo@mcbar.com))
And we can accomplish all of this with no new macros and about a dozen lines of code,
thanks to Shapeless (if we don't count the Complement
type class, which is a very
generic, slightly modified version of ops.hlist.RemoveAll
).
A full demonstration project (including Complement
) is available
here, and a version of Incompletes
may be coming to either
argonaut-shapeless or Finch soon.