HKDGeneric
HKDGeneric[A]
is a typeclass that provides functions to convert A
to and from some higher kinded data type Gen
. These functions are def to(a: A): Gen[Id]
and def from(gen: Gen[Id]): A
. The type Gen
can be manipulated and changed in the same way as a List
can be changed. Functions providing these operations are exposed on higher kinded versions of typical typeclasses like Functor
, Applicative
, Traverse
and so on. More details on these operations and typeclases can be found in typeclasses. Of particular note is Representable
"Fused" RepresentableK
operations
As it is so central to what HKDGeneric
does, HKDGeneric
exposes "fused" functions, combining RepresentableK.indicesK
with something some other functions, like foldLeftK
and traverseK
for performance.
HKDGeneric and implicits
The higher kinded representation Gen
from HKDGeneric
provides implicit instances of Gen[F]
if all the types making up Gen
also have instances for F
.
Product types
Generic programming on product types typically consist of creating a value of the higher kinded representation either through tabulateK
or to
, and ending with a call to a fold function or from
.
Product examples: circe encoders and decoders
Here are some examples of deriving circe Encoder
s and Decoder
s using HKDProductGeneric
.
trait PerspectiveProductDecoder[A] extends Decoder[A]
object PerspectiveProductDecoder:
def derivedProductDecoder[A](
using gen: HKDProductGeneric[A],
decoders: gen.Gen[Decoder]
): PerspectiveProductDecoder[A] = new PerspectiveProductDecoder[A]:
val names = gen.names
override def apply(cursor: HCursor): Either[DecodingFailure, A] =
// tabulateTraverseKEither is a fused version of tabulateK and traverseK specialized on Either
gen.tabulateTraverseKEither {
[X] => (idx: gen.Index[X]) =>
val name = names.indexK(idx)
val decoder = decoders.indexK(idx)
cursor.get(name)(using decoder)
}.map(gen.from)
inline def derived[A](using gen: HKDGeneric[A]): PerspectiveProductDecoder[A] = inline gen match
case gen: HKDProductGeneric.Aux[A, gen.Gen] =>
val decoders = summonInline[gen.Gen[Decoder]]
derivedProductDecoder(using gen, decoders)
trait PerspectiveProductEncoder[A] extends Encoder[A]
object PerspectiveProductEncoder:
def derivedProductEncoder[A](
using gen: HKDProductGeneric[A],
encoders: gen.Gen[Encoder]
): PerspectiveProductEncoder[A] = new PerspectiveProductEncoder[A]:
val names = gen.names
override def apply(a: A): Json =
val list: List[(String, Json)] =
gen.tabulateFoldLeft(Nil: List[(String, Json)]) { acc =>
[X] => (idx: gen.Index[X]) =>
// productElementId allows indexing a value of type A without calling gen.to on it first
val v = a.productElementId(idx)
val name = names.indexK(idx)
val encoder = encoders.indexK(idx)
(name, encoder(v)) :: acc
}
Json.obj(list: _*)
inline def derived[A](using gen: HKDGeneric[A]): PerspectiveProductEncoder[A] = inline gen match
case gen: HKDProductGeneric.Aux[A, gen.Gen] =>
val encoders = summonInline[gen.Gen[Encoder]]
derivedProductEncoder(using gen, encoders)
Sum types
HKDSumGeneric
works in many ways like HKDProductGeneric
, but with some differences. The higher kinded type in this case has one value for each case of the sum type. HKDSumGeneric
also provides a new family of functions indexOf
that takes a value of the sum type, and returns its index.
trait HKDSumGeneric[A] extends HKDGeneric[A]:
type ElemTop <: A
def indexOf[X <: ElemTop](x: X): Index[X]
The functions from
and to
functions also have different types. def to(a: A): Gen[Option]
and def from(gen: Gen[Option]): Option[A]
. to
fills all the slots of the higher kinded type with None
except for the slot that corresponds to the index of the value, which is filled with the value. from
returns Some
if only one slot in the higher kinded type is None
, and all the others are None
.
Sum examples: circe encoders and decoder
To deal with enums, we need a function that derives instances for all the sum type cases automatically. I have sadly not found any way to deal with it here. It can be seen here in the functions caseDecoders
and caseDecoders
. These examples also show how names and indices can interact.
trait PerspectiveProductDecoder[A] extends Decoder[A]
object PerspectiveProductDecoder:
def derivedSumDecoder[A](
using gen: HKDSumGeneric[A],
decoders: gen.Gen[Decoder]
): PerspectiveProductDecoder[A] = new PerspectiveProductDecoder[A]:
override def apply(cursor: HCursor): Either[DecodingFailure, A] =
for
typeNameStr <- cursor.get[String]("$type")
typeName <- gen
.stringToName(typeNameStr)
.toRight(DecodingFailure(s"$typeNameStr is not a valid ${gen.typeName}", cursor.history))
index = gen.nameToIndex(typeName)
decoder = decoders.indexK(index)
valueCursor = cursor.downField("$value")
res <- decoder(cursor.downField("$value").success.getOrElse(cursor))
yield res
private inline def caseDecoders[T <: Tuple, R <: Tuple](builder: Helpers.TupleBuilder[R]): R =
inline erasedValue[T] match
case _: (h *: t) =>
builder += summonFrom {
case d: Decoder[`h`] => d
case given HKDGeneric[`h`] => derived[h]
}
caseDecoders[t, R](builder)
case _: EmptyTuple => builder.result
inline def derived[A](using gen: HKDGeneric[A]): PerspectiveProductDecoder[A] = inline gen match
case gen: HKDSumGeneric.Aux[A, gen.Gen] =>
summonFrom {
case decoders: gen.Gen[Decoder] => derivedSumDecoder(using gen, decoders)
case _ =>
val decoders = gen.tupleToGen(
caseDecoders[gen.TupleRep, Helpers.TupleMap[gen.TupleRep, Decoder]](Helpers.TupleBuilder.mkFor)
)
derivedSumDecoder(using gen, decoders)
}
trait PerspectiveSumEncoder[A] extends Encoder[A]
object PerspectiveSumEncoder:
def derivedSumEncoder[A](
using gen: HKDSumGeneric[A],
encoders: gen.Gen[Encoder]
): PerspectiveSumEncoder[A] = new PerspectiveSumEncoder[A]:
override def apply(a: A): Json =
val typeName = (gen.indexToName(gen.indexOfA(a)): String).asJson
val encodings =
gen
.to(a)
.map2Const(encoders)([Z] => (optCase: Option[Z], encoder: Encoder[Z]) => optCase.map(x => encoder(x)))
val json = encodings.indexK(gen.indexOfA(a)).get
json.asObject match
case Some(fields) => json.deepMerge(Json.obj("$type" -> typeName))
case None => Json.obj("$type" -> typeName, "$value" -> json)
private inline def caseEncoders[T <: Tuple, R <: Tuple](builder: Helpers.TupleBuilder[R]): R =
inline erasedValue[T] match
case _: (h *: t) =>
builder += summonFrom {
case d: Encoder[`h`] => d
case given HKDGeneric[`h`] => derived[h]
}
caseEncoders[t, R](builder)
case _: EmptyTuple => builder.result
inline def derived[A](using gen: HKDGeneric[A]): PerspectiveSumEncoder[A] = inline gen match
case gen: HKDSumGeneric.Aux[A, gen.Gen] =>
summonFrom {
case encoders: gen.Gen[Encoder] => derivedSumEncoder(using gen, encoders)
case _ =>
val encoders = gen.tupleToGen(
caseEncoders[gen.TupleRep, Helpers.TupleMap[gen.TupleRep, Encoder]](Helpers.TupleBuilder.mkFor)
)
derivedSumEncoder(using gen, encoders)
}