Skip to content

Instantly share code, notes, and snippets.

@vendethiel
Forked from SystemFw/Lib.scala
Last active May 26, 2020 11:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vendethiel/29088fda384bc290d7e55d3f7ae3566a to your computer and use it in GitHub Desktop.
Save vendethiel/29088fda384bc290d7e55d3f7ae3566a to your computer and use it in GitHub Desktop.
Shapeless: derive Slick's GetResult for arbitrary case classes
import slick.jdbc.{GetResult, PositionedResult}
import scala.annotation.implicitNotFound
import scala.reflect.runtime.universe.TypeTag
/**
* A type class that allows the user of GenericGetResult to support their own type.
* This is mostly used to support `newtype`s (like wrappers around UUIDs).
*
* A user just has to use the helper to make the type known to GenericGetResult.
*
* --------
*
* Example:
*
* > case class UserId(value: UUID)
* > case class User(id: UserId)
*
* Obviously, GenericGetResult doesn't know about UserId,
* which makes GenericGetResult.generate[User] currently an error.
* So we can manually give a JDBCParser for UserId, and make it work:
*
* > implicit val parseUserId: JDBCParser.make(r ⇒ UserId(r.nextUUID()))
*
* @tparam T The type for which we want to parse from DB
*/
trait JDBCParser[T] {
def parse(r: PositionedResult): T
}
object JDBCParser {
/**
* A simple helper function.
* @param fn The parser function to call
* @tparam T The type the parser will return.
* @return The parsed value.
*/
def make[T](fn: PositionedResult ⇒ T) = new JDBCParser[T] {
override def parse(r: PositionedResult): T = fn(r)
}
}
/**
* The backbones of the GenericGetResult mechanism.
* This trait will serve for the Aux pattern (from shapeless).
*
* @tparam I The type to generate a GetResult for.
*/
@implicitNotFound("Not all fields of ${I} have a JDBC reader")
trait GenericGetResult[I] {
type Out
def value: Out
}
object GenericGetResult {
import shapeless._
import cats.data.Reader
import cats.sequence._
/**
* Unused when directly going through `generate`, this method is otherwise the starting point when using this:
* This is used to trigger the unification process!
*
* > GenericGetResult.of[MyCaseClass]
*
* The line above returns a Reader[PositionedResult, MyCaseClass].
*/
def of[T](implicit ev: GenericGetResult[T]): ev.Out = ev.value
/**
* This is used to fake prolog-style declarations.
* This case class, the Aux pattern, is a trick from the shapeless library,
* where we the type I is the "input", and the type O is the "output".
*
* The reason we need this is because in Scala, a parameter can't depend on the value of some other parameter.
* This is invalid:
* > def fn[T](a: Typeclass[T], b: a.Out) = ???
* This is valid:
* > def fn[T, O](a: Typeclass[T], b: Typeclass[O])(implicit ev: Aux[T, O]) = ???
*
* In more details:
* A good synergy for how implicit resolution works for Aux is a type-level where:
* type parameters (class A[T]) are inputs.
* type members (class A { type T }) are outputs.
* This is how the Scala compiler does implicit resolution. It will *not* try to start a unification from a type member.
* It will, however, try to start the unification resolution from a type parameter.
*
* So, when the compiler see Aux[A, B], if A is known but B isn't, then it will go through the known implicits
* and try to unify both type variables on every implicit in scope, until one works.
*
* @tparam I The "input".
* @tparam O The "output".
*/
type Aux[I, O] = GenericGetResult[I] { type Out = O }
/**
* Convenient wrapper function to create a Aux.
* @param v The value
* @tparam I The "input" (see Aux).
* @tparam O The "output" (see Aux).
* @return A Aux with its Out & value members set.
*/
def instance[I, O](v: O): GenericGetResult.Aux[I, O] = new GenericGetResult[I] {
type Out = O
def value = v
}
/**
* Just a shorthand syntax for Reader.
* @tparam A The type that the reader reads.
*/
type JDBCReader[A] = Reader[PositionedResult, A]
/* Read an Int. */
implicit def ints: GenericGetResult.Aux[Int, JDBCReader[Int]] =
instance(Reader(_.nextInt))
/* Read a Double. */
implicit def doubles: GenericGetResult.Aux[Double, JDBCReader[Double]] =
instance(Reader(_.nextDouble))
/* Read a Float. */
implicit def floats: GenericGetResult.Aux[Float, JDBCReader[Float]] =
instance(Reader(_.nextFloat))
/* Read a String. */
implicit def strings: GenericGetResult.Aux[String, JDBCReader[String]] =
instance(Reader(_.nextString))
/* Read a Seq[T]. We need a TypeTag because _.nextArray needs one. */
implicit def seq[T : TypeTag]: GenericGetResult.Aux[Seq[T], JDBCReader[Seq[T]]] = {
import slickPgSupport._
instance(Reader(_.nextArray[T]))
}
/* This uses the JDBCParser typeclass provided earlier.
As explained in JDBCParser's comment, this is useful for custom types that aren't known here.
*/
implicit def userSuppliedReader[T](implicit parser: JDBCParser[T]): GenericGetResult.Aux[T, JDBCReader[T]] =
instance(Reader(r ⇒ parser.parse(r)))
/* This is the end-of-recursion case. */
implicit def hnil: GenericGetResult.Aux[HNil, HNil] =
instance(HNil)
/**
* This is the base recursion case.
*
* @tparam H The "H"ead of the HList, the first type we'll convert, which is in input position of the Aux.
* @tparam T The "T"ail of the HList, with every other types we need to convert (and a HNil at the end),
* which is also in input position of the Aux.
* @tparam R The "R"est to convert, which is in output position of the Aux.
* @param g The converter for H.
* @param n A converter from the "T"ail (input) to the "R"est (output).
*
*
* Note: Following the "Aux's first type parameter is input, the second type parameter is output",
* it means that the return type, Aux[H :: T, g.Out :: R], has:
*
* The input H :: T.
* - We get a converter for H (parameter g).
* - We get a Aux from T to R, the output type.
* The output g.Out :: R.
* - g.Out is the what came out of our parameter g, so, what H got converted to.
* - R is the "R"est that we have to convert. This is where the recursion happens!
*
* The recursion happens in the parameter n, because the compiler knows the type of T,
* and it needs to find a R using unification.
* If the "T"ail is HNil, the recursion is done: The compiler finds GenericGetResult.Aux[HNil, HNil], and stops looking.
* However, if there are more than one element in the list, it will invoke hcons again.
*
* -------------
*
* Step-by-step:
*
* > GenericGetResult.of[Int :: Double :: HNil]
* (:: is shapeless.::, a HList constructor)
*
* Then H = Int, T = Double :: HNil.
* The compiler finds g: GenericGetResult[H = Int] (that's `ints`).
* Now it needs to unify GenericGetResult.Aux[T = Double :: HNil, R].
*
* It finds hcons again, this time with:
* H = Double, T = HNil.
* The compiler finds g: GenericGetResult[H = Double] (that's `doubles`).
* Now it needs to unify GenericGetResult.Aux[H = HNil, R].
* It finds `hnil` and stops recursing.
*
* so, the inner hcons (with H = Double, T = HNil) has a result type of:
* > GenericRetResult.Aux[H = Double :: T = HNil, g.Out = JDBCReader[Double] :: R = HNil]
*
* the outer hcons (with H = Int, T = Double :: HNil) has a result type of:
* > GenericGetResult.Aux[H = Int :: (T = Double :: HNil), g.Out = JDBCReader[Int] :: (R = JDBCReader[Double] :: HNil)]
*/
implicit def hcons[H, T <: HList, R <: HList](
implicit g: GenericGetResult[H],
n: GenericGetResult.Aux[T, R]
): GenericGetResult.Aux[H :: T, g.Out :: R] =
instance(g.value :: n.value)
/**
* This function uses Kittens to transform any case class into a HList, sequence the Reader, and then back to the case class.
*
* This case class:
* > case class Ex(a: Int, b: Double, c: Int)
* gives this HList:
* > Int :: Double :: Int :: HNil
*
* @param gen The generator. Takes a T as input and returns a L.
* @param v The converter from L to O.
* @param ev The "splitting" mechanism of a case class-and-back.
* @tparam T The only known parameter at the beginning: the case class type.
* @tparam L The HList type that will be used for implicit resolution (starts in hcons, see its documentation).
* @tparam O The post-conversion HList of L.
* @return A Reader[T] that can be ran.
*/
implicit def genReader[T, L <: HList, O <: HList](
implicit gen: Generic.Aux[T, L],
v: GenericGetResult.Aux[L, O],
ev: Sequencer.Aux[O, JDBCReader, L]
): GenericGetResult.Aux[T, JDBCReader[T]] =
instance(v.value.sequence.map(gen.from))
/**
* Convenient helper to create a GetResult.
* @param ev The evidence that we can go from a T to a JDBCReader[T], which is the type of ev.value (the Out type)
* @tparam T The case class type.
* @return A GetResult[T].
*/
def generate[T](implicit ev: GenericGetResult.Aux[T, JDBCReader[T]]): GetResult[T] = {
GetResult[T](r ⇒ ev.value.run(r))
}
}
package sample.shapeless
import scala.annotation.implicitNotFound
// same as Lib.scala, but doesn't use IO
trait JDBC {
def nextInt: Int
def nextDouble: Double
def nextFloat: Float
def nextVector[T: JDBCGenerator]: Vector[T]
}
trait JDBCGenerator[T] { def generate: Vector[T] }
object yourowngenerators {
implicit object IntGen extends JDBCGenerator[Int] {
def generate: Vector[Int] = Vector(100, 110, 120)
}
}
class JDBCimpl(a: Int, b: Double, c: Float) extends JDBC {
override def nextInt: Int = a
override def nextDouble: Double = b
override def nextFloat: Float = c
override def nextVector[T: JDBCGenerator]: Vector[T] = implicitly[JDBCGenerator[T]].generate
}
@implicitNotFound("Not all fields of ${I} have a JDBC reader")
trait Results[I] {
type Out
def value: Out
}
object Results {
import shapeless._
import cats.data.Reader
import cats.sequence._ //requires kittens
def of[T](implicit ev: Results[T]): ev.Out = ev.value
type Aux[I, O] = Results[I] { type Out = O }
def instance[I, O](v: O): Results.Aux[I, O] = new Results[I] {
type Out = O
def value = v
}
type JDBCReader[A] = Reader[JDBC, A]
implicit def ints: Results.Aux[Int, JDBCReader[Int]] = instance(Reader(_.nextInt))
implicit def doubles: Results.Aux[Double, JDBCReader[Double]] = instance(Reader(_.nextDouble))
implicit def float: Results.Aux[Float, JDBCReader[Float]] = instance(Reader(_.nextFloat))
implicit def arrOf[T: JDBCGenerator]: Results.Aux[Vector[T], JDBCReader[Vector[T]]] = instance(Reader(_.nextVector[T]))
implicit def hnil: Results.Aux[HNil, HNil] = instance(HNil)
implicit def hcons[H, T <: HList, R <: HList](
implicit g: Results[H],
n: Results.Aux[T, R]
): Results.Aux[H :: T, g.Out :: R] =
instance(g.value :: n.value)
implicit def gen[T, L <: HList, O <: HList](
implicit gen: Generic.Aux[T, L],
v: Results.Aux[L, O],
ev: Sequencer.Aux[O, JDBCReader, L]
): Results.Aux[T, Reader[JDBC, T]] =
instance(v.value.sequence.map(gen.from))
}
object Test {
def main(args: Array[String]): Unit = {
case class Foo(a: Int, b: Double, c: Int, d: Vector[Int])
def r: JDBC = new JDBCimpl(1, 2, 3)
import yourowngenerators._
val d = Results.of[Foo].run(r)
print(d)
}
}
import shapeless._ // requires.shapeless
import cats._, implicits._, data.Kleisli // requires.cats
import cats.sequence._ //requires kittens
import cats.effect.IO //requires cats-effect
case class GetResult() // replace with slick's
case class DB(val g: GetResult) {
def nextInt: IO[Int] = ??? //IO(g.nextInt)
def nextDouble: IO[Double] = ???
def nextFloat: IO[Float] = ???
}
trait Results[I] {
type Out
def value: Out
}
object Results {
def of[T](implicit ev: Results[T]): ev.Out = ev.value
type Aux[I, O] = Results[I] { type Out = O }
def instance[I, O](v: O): Results.Aux[I, O] = new Results[I] {
type Out = O
def value = v
}
implicit def ints: Results.Aux[Int, Kleisli[IO, DB, Int]] =
instance(Kleisli(r => r.nextInt))
implicit def doubles: Results.Aux[Double, Kleisli[IO, DB, Double]] =
instance(Kleisli(r => r.nextDouble))
implicit def floats: Results.Aux[Float, Kleisli[IO, DB, Float]] =
instance(Kleisli(r => r.nextFloat))
implicit def hnil: Results.Aux[HNil, HNil] = instance(HNil)
implicit def hcons[H, T <: HList, R <: HList](
implicit g: Results[H],
n: Results.Aux[T, R]): Results.Aux[H :: T, g.Out :: R] =
instance(g.value :: n.value)
implicit def gen[T, L <: HList, O <: HList](
implicit gen: Generic.Aux[T, L],
v: Results.Aux[L, O],
ev: Sequencer.Aux[O, Kleisli[IO, DB, ?], L]): Results.Aux[T, Kleisli[IO, DB, T]] =
instance(v.value.sequence.map(gen.from))
}
object Test {
case class Foo(a: Int, b: Double)
def db: DB = ???
val d: IO[Foo] = Results.of[Foo].run(db)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment