-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Synthesize Representable type class #3663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This commit breaks things, for instance pos/i1795.scala that fail during pickling... To be investigated later.
This looks interesting, but I recommend against committing to a concrete representation type, or if you do, go with The big issue is with kinding as you've already started to observe with Also note that shapeless's While I support the idea of making functionality of this sort a language intrinsic, I recommend against baking in an implementation which commits to replicating boilerplate at the kind level and limits derivations to such a simple form. |
@milessabin Thanks for your input!
I want to experiment with the GHC.generics approach where higher kinded sum & product are used for both ground types and HKT, something along these lines: sealed trait Prod[X]
final case class PCons[H[_], T[t] <: Prod[t], X](head: H[X], tail: T[X]) extends Prod[X]
final case class PNil[X]() extends Prod[X]
sealed trait Sum[X]
sealed trait SCons[H[_], T[t] <: Sum[t], X] extends Sum[X]
final case class SLeft[H[_], T[t] <: Sum[t], X](head: H[X]) extends SCons[H, T, X]
final case class SRight[H[_], T[t] <: Sum[t], X](tail: T[X]) extends SCons[H, T, X]
sealed trait SNil[X] extends Sum[X]
trait Representable[A] {
type Repr[t] <: Sum[t] | Prod[t]
def to[T](a: A): Repr[T]
def from[T](r: Repr[T]): A
}
trait Representable1[A[_]] {
type Repr[t] <: Sum[t] | Prod[t]
def to[T](a: A[T]): Repr[T]
def from[T](r: Repr[T]): A[T]
}
// Syntax for ground types
type &:[H, T[t] <: Prod[t]] = [X] => PCons[[Y] => H, T, X]
type |:[H, T[t] <: Sum[t]] = [X] => SCons[[Y] => H, T, X]
// Syntax for HKT
type :&:[H[_], T[t] <: Prod[t]] = [X] => PCons[H, T, X]
type :|:[H[_], T[t] <: Sum[t]] = [X] => SCons[H, T, X]
type Id[t] = t
type Const[t] = [X] => t sealed trait Tree[T]
case class Leaf[T](t: T) extends Tree[T]
case class Node[T](l: Tree[T], r: Tree[T]) extends Tree[T]
Representable1[Node] { type Repr = Tree :&: Tree :&: PNil }
Representable1[Leaf] { type Repr = Id :&: PNil }
Representable1[Tree] { type Repr = Leaf :|: Node :|: SNil }
Representable[Node[A]] { type Repr = Tree[A] &: Tree[A] &: PNil }
Representable[Leaf[A]] { type Repr = A &: PNil }
Representable[Tree[A]] { type Repr = Leaf[A] |: Node[A] |: SNil }
Are you refering to cases where additional/external type classes are mixed in during derivation? Is it common in practice? |
I find something that stops at
Yes, very. The most common is to thread |
Following a discussion I had with @OlivierBlanvillain, I’d like to share a more elaborate example than This example is inspired from a library that describes data types and then derives typeclass instances from these descriptions (API doc is here). For simplicity, only record types (case classes) are considered, the case of sum types is similar but uses Consider the following typeclass for serializing/deserializing data into/from JSON documents: trait Codec[A] {
def encode(a: A): Json
def decode(json: Json): Either[ValidationErrors, A]
} Here is how we would manually define an instance of case class User(name: String, age: Int)
object User {
implicit val codec: Codec[User] =
Codec.obj2(
"name" -> Codec.string,
"age" -> Codec.integer
) { case (n, a) => User(n, a) } { user => (user.name, user.age) }
} It assumes that the following operations are available: object Codec {
/** JSON String */
implicit def string: Codec[String] = …
/** JSON number */
implicit def integer: Codec[Int] = …
/** JSON object with two fields */
def obj2[A, B, C](
fieldA: (String, Codec[A]), fieldB: (String, Codec[B])
)(
f: (A, B) => C
)(
g: C => (A, B)
): Codec[C]
} Ideally, we would like generically derived instances of However, with the trait DerivedCodec[A] {
def codec: Codec[A]
}
object DerivedCodec {
/** Base rule: derives a codec for a case class with exactly one field */
implicit def singletonField[L <: Symbol, A](
fieldLabel: ValueOf[L],
fieldCodec: Codec[A]
): DerivedCodec[FieldType[L, A] :: HNil] = new DerivedCodec[FieldType[L, A] :: HNil] {
def codec = Codec.obj1(fieldLabel.value.name -> fieldCodec).invmap(a => field[L](a) :: HNil)(_.head)
}
/** Induction rule: derives a codec for a case class with n + 1 fields, given a derived codec for a case class with n fields */
implicit def consField[L <: Symbol, H, T <: HList](implicit
fieldLabel: ValueOf[L],
fieldCodec: Codec[H],
tailDerivedCodec: DerivedCodec[T]
): DerivedCodec[FieldType[L, H] :: T] = new DerivedCodec[FieldType[L, H] :: T] {
def codec =
Codec.obj1(fieldLabel.value.name -> fieldCodec).zip(tailDerivedCodec.codec)
.invmap { case (h, t) => field[L](h) :: t } { ht => (ht.head, ht.tail) }
}
/** Derives a codec for a case class `A`, given a derived codec for its generic representation `R` */
implicit def hlistToCaseClass[A, R](implicit
gen: LabelledGeneric.Aux[A, R],
derivedCodec: DerivedCodec[R]
): DerivedCodec[A] = new DerivedCodec[A] {
def codec = derivedCodec.codec.invmap(gen.from)(gen.to)
}
} This example uses trait Codec[A] {
/** combines `this` codec with `that` codec */
def zip[B](that): Codec[(A, B)] = …
/** transforms this `Codec[A]` into a `Codec[B]` by using a pair of inverse functions */
def invmap[B](f: A => B)(g: B => A): Codec[B] = …
}
object Codec {
/** JSON object with one field of type `A` */
def obj1[A](field: (String, Codec[A])): Codec[A] = …
} If we derive a hlistToCaseClass(
<compiler-synthesized>,
consField(
'name,
Codec.string,
singletonField('age, Codec.integer)
)
) Which, in turn, expands to: Codec.obj1('name.name -> Codec.string)
.zip(Codec.obj1('age.name -> Codec.integer).invmap(a => field['age](a) :: HNil)(_.head))
.invmap { case (h, t) => field['age](h) :: t } { ht => (ht.head, ht.tail) }
.invmap { case n :: a :: HNil => User(n, a) } { user => user.name :: user.age :: HNil } (I removed the intermediate As wee can see, the derived instance uses a lot of intermediate transformations (
We might be able to inline the Let’s try to manually rewrite the induction step without using implicit def consField[L <: Symbol, H, T <: HList](implicit
fieldLabel: ValueOf[L],
fieldCodec: Codec[H],
tailDerivedCodec: DerivedCodec[T]
): DerivedCodec[FieldType[L, H] :: T] = new DerivedCodec[FieldType[L, H] :: T] {
def codec = new Codec[FieldType[L, H] :: T] {
def encode(ht: FieldType[L, H] :: T): Json =
Json.obj(fieldLabel.value.name -> fieldCodec.encode(ht.head.value))
.merge(tailDerivedCodec.codec.encode(ht.tail))
def decode(json: Json): Either[ValidationError, FieldType[L, H] :: T] = {
val headResult =
json match { case JsonObject(fields) if fields.contains(fieldLabel.value.name) => fieldCodec.decode(fields.get(fieldLabel.value.name)) case _ => Left(MissingField(fieldLabel.value.name))
val tailResult = tailDerivedCodec.codec.decode(json)
(headResult, tailResult) match {
case (Right(h), Right(t)) => Right(field[L](h) :: t)
case (Left(e), Right(_)) => Left(e)
case (Right(_), Left(e)) => Left(e)
case (Left(e1), Left(e2)) => Left(e1.concat(e2))
}
}
}
} The derived codec would still be less performant than the manually written one (the derived one uses Note that we even have to expand the code for combining |
/cc @fommil as I think he will be interested in this |
The alternative to case classes (now mothballed because scalameta macros were abandoned) was written up at https://vovapolu.github.io/scala/stalagmite/perf/2017/09/02/stalagmite-performance.html I'd love to return to it. My new approach to typeclass derivation at the point of data definition is at https://gitlab.com/fommil/scalaz-deriving. I'll be writing a chapter in my book soon, plus hopefully giving a talk at lambdaconf. |
Subsumed by #5540 |
This PR is the first step towards integrating parts shapeless' generic programming facilities in Dotty.
A
Representable
type class in added in packagedotty.generic
, alongsideSum
andProd
types to implement the equivalent ofshapeless.Generic
.Representable[A]
is synthesized as a fallback to implicit search, whenA
is a case class or a sealed trait. To make that possible, child annotations to sealed classes are added in typer instead ofPostTyper
.Plans for future work are as follows:
Include metadata in the generic representation (starting from labels)
Support higher kinded types via
Representable1
,Sum1
andProd1
. Early experiments (library based) show that this can be done very elegantly in Dotty by using higher kinded counterparts ofRepresentable
,Sum
andProd
, as done in GHC-generics.Experiment with offload more of the type class derivation work to the compiler (doing less in implicit search). The idea would be require users to write their derivation in a
ReprFold
type class (similar toshapeless.TypeClass
), then implement the actual fold in aDeriving
type class, compiler generated: