Skip to content

Instantly share code, notes, and snippets.

@knutwalker
Last active August 29, 2015 14:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save knutwalker/788acb4c7ae9782fdf67 to your computer and use it in GitHub Desktop.
Save knutwalker/788acb4c7ae9782fdf67 to your computer and use it in GitHub Desktop.

Playing around with Shapeless to require certain Fields w/o any boilerplate.

The goal is to provide a boilerplate-free infrastructure for extracting certain fields out of a record type.

Situation

Say, For example, a database service might require an ID for every entity. The typical OO solutions would provide a base class with an abstract ID field or rely on annotations and reflections to specify the ID field. Such solutions often bind the entity/model class directly to the database implementation or, at best, to something like JPA.

A solution where JPA can be called "best" is hardly a good thing.

Approach

The HasId typeclass in combination with shapeless generic programming is an approach to extract the ID value from any arbitrary case class (record type, really) without any boilerplate code required at all. It requires a certain field with a specific type, but this is no different than requiring an interface/trait with this particular abstract method to be implemented.

The users' model definition needn't to depend on third-party libraries (not even shapeless), so it can be truly dependency free.

The HasId typeclass can be provided either by an independent library or the database library and the latter of which can provide an API like def save[A: HasId](userEntity: A) and can thus support any user defined type. Invalid types will be sorted out at compile-time and no runtime reflection is involved.

The user may choose to couple their models to HasId by caching the implicits instances in order to reduce compile time, but this is not necessary.

import shapeless._
import shapeless.labelled._
import shapeless.ops.record._
import scala.annotation.implicitNotFound
@implicitNotFound("Cannot prove that ${A} has an 'id: Int' field.")
trait HasId[A] {
def apply(a: A): Int
}
object HasId {
def apply[A](implicit A: HasId[A]): HasId[A] = A
def apply[A](a: A)(implicit A: HasId[A]): Int = A(a)
implicit def hasIdHList[A, R <: HList](implicit
gen: LabelledGeneric.Aux[A, R],
sel: Selector.Aux[R, Witness.`'id`.T, Int])
: HasId[A] = new HasId[A] {
def apply(a: A): Int = sel(gen.to(a))
}
implicit def hasIdCoproduct[A, Repr <: Coproduct](implicit
gen: LabelledGeneric.Aux[A, Repr],
repr: Lazy[HasId[Repr]])
: HasId[A] = new HasId[A] {
def apply(a: A): Int = repr.value(gen.to(a))
}
implicit val hasIdCNil: HasId[CNil] = new HasId[CNil] {
def apply(a: CNil): Int = unexpected
}
implicit def hasIdCCons[K <: Symbol, L, R <: Coproduct](implicit
K: Witness.Aux[K],
L: Lazy[HasId[L]],
R: Lazy[HasId[R]])
: HasId[FieldType[K, L] :+: R] = new HasId[FieldType[K, L] :+: R] {
def apply(a: FieldType[K, L] :+: R): Int = {
a match {
case Inl(head) ⇒ L.value(head)
case Inr(tail) ⇒ R.value(tail)
}
}
}
}
import org.specs2.Specification
import org.specs2.execute._, Typecheck._
import org.specs2.matcher.TypecheckMatchers._
object HasIdSpec extends Specification {
sealed trait CorrectAdt
sealed trait IncorrectAdt
case class Foo(id: Int) extends CorrectAdt with IncorrectAdt
case class Bar(baz: String, id: Int, qux: Int) extends CorrectAdt with IncorrectAdt
case class Moo() extends IncorrectAdt
def is = "HasId Specification".title ^ s2"""
HasId scans a case class or shapeless Record for a field
with the name `id` and the type `Int`
in `case class Foo(id: Int)`
${HasId(Foo(42)) ==== 42}
in `case class Bar(baz: String, id: Int, qux: Int)`
${HasId(Bar("foo", 42, 1337)) ==== 42}
It is a compile error if there is no such field present
in `case class Moo()`
${typecheck("HasId[Moo]") must failWith("Cannot prove that .*Moo has an 'id: Int' field.")}
ADTs are supported if all cases have an `id: Int` field.
It is not required to have this field in the base trait.
${HasId[CorrectAdt](Foo(42)) ==== 42}
${HasId(Bar("foo", 42, 1337): CorrectAdt) ==== 42}
If there is one case for which no `HasId` can be found, the ADT is not supported
${typecheck("HasId[IncorrectAdt]") must failWith("Cannot prove that .*IncorrectAdt has an 'id: Int' field.")}
"""
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment