Scala shapeless typing Map[Symbol, String] with case classes - scala

I'm reading in query parameters and converting them into a Map[Symbol, String]. I would like to add some type safety to these query parameters through a set of case classes.
These case classes will be different depending on the incoming http request, so this needs to support different case classes.
If the incoming query parameters don't match the defined case class the Parser should return None.
I have attempted to use shapeless to implement a generic parser. It work's if all of the parameters are of type String. But I need to support any type of query parameter.
I've tried to incorporate the implicit conversion logic seen in this post, but unable to get it working.
https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation/ (new to shapeless)
Existing Parser (without string to type conversion):
class Parser[A] {
def from[R <: HList]
(m: Map[Symbol, String])
(implicit
gen: LabelledGeneric.Aux[A, R],
fromMap: FromMap[R]
): Option[A] = fromMap(m).map(gen.from)
}
object Parser {
def to[A]: Parser[A] = new Parser[A]
}
Tests describing issue:
class ParserSpec extends FlatSpec with Matchers {
private val sampleName: String = "Bob"
private val sampleVersion: Int = 1
//Partial Solution
case class QueryParams(name: String, version: String)
//Full Solution (not working)
case class QueryParams2(name: String, version: Int)
"A Parser" should "parse query parameters from a map with only string values" in {
val mapOfQueryParams = Map('name -> sampleName, 'version -> sampleVersion.toString)
val result = Parser.to[QueryParams].from(mapOfQueryParams)
result shouldBe 'defined
result.get.name shouldEqual sampleName
result.get.version shouldEqual sampleVersion.toString
}
it should "parse query parameters from a map with any type of value" in {
val mapOfQueryParams = Map('name -> sampleName, 'version -> sampleVersion.toString)
val result = Parser.to[QueryParams2].from(mapOfQueryParams)
//result is not defined as it's not able to convert a string to integer
result shouldBe 'defined
result.get.name shouldEqual sampleName
result.get.version shouldEqual sampleVersion
}
}

FromMap uses shapeless.Typeable to convert values to the expected type. So the easiest way to make your code work is to define an instance of Typeable to convert from String to Int (and additional Typeable instances for any value type, that appears in your case classes):
implicit val stringToInt: Typeable[Int] = new Typeable[Int] {
override def cast(t: Any): Option[Int] = t match {
case t: String => Try(t.toInt).toOption
case _ => Typeable.intTypeable.cast(t)
}
override def describe: String = "Int from String"
}
This is however not an intended use of Typeable, which is designed to confirm that a variable with type Any is already an instance of the expected type without any conversion. In other words it's intended to be a typesafe implementation of asInstanceOf, that can also work around type erasure.
For correctness you can define your own ReadFromMap typeclass, that uses your own Read typeclass for conversion from Strings to the expected types. Here is a simple implementation of the Read typeclass (assuming Scala 2.12):
import scala.util.Try
trait Read[T] {
def apply(string: String): Option[T]
}
object Read {
implicit val readString: Read[String] = Some(_)
implicit val readInt: Read[Int] = s => Try(s.toInt).toOption
// Add more implicits for other types in your case classes
}
And you can copy and adapt the implementation of FromMap to use this Read typeclass:
import shapeless._
import shapeless.labelled._
trait ReadFromMap[R <: HList] extends Serializable {
def apply(map: Map[Symbol, String]): Option[R]
}
object ReadFromMap {
implicit def hnil: ReadFromMap[HNil] = _ => Some(HNil)
implicit def hlist[K <: Symbol, V, T <: HList](implicit
keyWitness: Witness.Aux[K],
readValue: Read[V],
readRest: ReadFromMap[T]
): ReadFromMap[FieldType[K, V] :: T] = map => for {
value <- map.get(keyWitness.value)
converted <- readValue(value)
rest <- readRest(map)
} yield field[K](converted) :: rest
}
Then simply use this new typeclass in your Parser:
class Parser[A] {
def from[R <: HList]
(m: Map[Symbol, String])
(implicit
gen: LabelledGeneric.Aux[A, R],
fromMap: ReadFromMap[R]
): Option[A] = fromMap(m).map(gen.from)
}

Related

Leveraging a generic return type in Scala

So I would like to use a generic return type and be able to use the info of that type within the function. Not sure this is possible but here is what I would like:
def getStuff[A](a: MyObj, b: String): Option[A] = {
// do some stuff
A match {
case String => Some(a.getString(b))
case Integer => Some(a.getInt(b))
...
case _ => None
}
}
However, as you know, A match is not a possibility. Any ideas on how I could achieve this ?
This is a classic case for using a typeclass:
trait StuffGetter[T] { // typeclass
def get(obj: MyObj, s: String): Option[T]
}
implicit val stringGetter = new StuffGetter[String] {
def get(o: MyObj, s: String): Option[String] = ???
}
implicit val intGetter = new StuffGetter[Int] {
def get(o: MyObj, s: String): Option[Int] = ???
}
def getStuff[A](a: MyObj, b: String)(implicit ev: StuffGetter[A]): Option[A] =
ev.get(a, b)
val stuff0 = getStuff[String](obj, "Hello") // calls get on stringGetter
val stuff1 = getStuff[Int](obj, "World") // call get on intGetter
val stuff2 = getStuff[Boolean](obj, "!") // Compile-time error
The StuffGetter trait defines the operations that you want to perform on the generic type, and each implicit value of that trait provides the implementation for a specific type. (For a custom type these are typically place in the companion object for the type; the compiler will look there for them)
When getStuff is called the compiler will look for an implicit instance of StuffGetter with the matching type. This will fail if no such instance exists, otherwise it will be passed in the ev parameter.
The advantage of this is that the "match" is done at compile time and unsupported types are also detected at compile time.
Conceptually we can differentiate between pattern matching at run-time which looks something like this
def getStuff[A](...) =
A match {
...
}
and pattern matching at compile-time which looks something like this
def getStuff[A](...)(implicit ev: Foo[A]) = {
ev.bar(...)
}
Key concept to understand is that types do not exists at run-time because they get "erased" after compilation so there is not enough information to pattern match on types once the program is running. However at compile-time, that is before the program runs, types do exist and Scala provides means to ask the compiler to effectively pattern match on them via implicit/givens mechanism which looks something like so
// type class requirements for type A
trait StringConverter[A] {
def getOptValue(b: String): Option[A]
}
// evidence which types satisfy the type class
implicit val intStringConverter: StringConverter[Int] = (b: String) => b.toIntOption
implicit val strStringConverter: StringConverter[String] = (b: String) => Some(b)
implicit def anyStringConverter[A]: StringConverter[A] = (b: String) => None
// compile-time pattern matching on type A
def getStuff[A](b: String)(implicit ev: StringConverter[A]): Option[A] = {
ev.getOptValue(b)
}
getStuff[Int]("3") // : Option[Int] = Some(value = 3)
getStuff[String]("3") // : Option[String] = Some(value = "3")
getStuff[Double]("3") // : Option[Double] = None
This compile-time pattern matching is called type class pattern.
Understanding the distinction between types and classes is one of the fundamental concepts in Scala https://docs.scala-lang.org/tutorials/FAQ/index.html#whats-the-difference-between-types-and-classes and gorking it will help understand how to write type classes.
Using custom typeclass similar to Getter:
trait KeyedGetter[S, K, A]:
def get(s: S, key: K): Option[A]
case class MyObj(ints: Map[String, Int], strs: Map[String, String])
object MyObj:
given KeyedGetter[MyObj, String, Int] with
def get(m: MyObj, k: String) = m.ints.get(k)
given KeyedGetter[MyObj, String, String] with
def get(m: MyObj, k: String) = m.strs.get(k)
def getStuff[A](m: MyObj, key: String)(using g: KeyedGetter[MyObj, String, A]): Option[A] =
g.get(m, key)
Using class tags:
case class MyObj(ints: Map[String, Int], strs: Map[String, String])
import reflect._
def getStuff[A](m: MyObj, key: String)(using ct: ClassTag[A]): Option[A] = (ct match
case _ if ct == classTag[String] => m.strs.get(key)
case _ if ct == classTag[Int] => m.ints.get(key)
case _ => None
).asInstanceOf[Option[A]]
If the erased types are insufficient, for a similar approach with type tags see this answer (and ignore the rest).

Could not find implicit value for parameter Mapper

I try to create a simple shapeless based function to convert the case class into a list of string I could then encode as csv.
The point of this is to summon type class CsvEncoder on every member of the case class and then invoke method encode to change it to string.
Type class:
trait CsvEncoder[T] {
def encode(t: T): String
}
trait LowPriorityEncoder {
implicit def genericEncoder[T]: CsvEncoder[T] = _.toString
}
object CsvEncoder extends LowPriorityEncoder {
implicit def optionEncoder[T](
implicit e: CsvEncoder[T]
): CsvEncoder[Option[T]] = _.fold("")(e.encode)
}
And shapeless:
class CsvBuilder[Row](rows: List[Row]) {
object csvMapper extends Poly1 {
implicit def caseEncode[V](
implicit e: CsvEncoder[V]
): Case.Aux[FieldType[Symbol, V], FieldType[Symbol, String]] =
at[FieldType[Symbol, V]](s => field[Symbol](e.encode(s)))
}
def build[Repr <: HList, OutRepr <: HList](
implicit labelledGeneric: LabelledGeneric.Aux[Row, Repr],
mapper: Mapper.Aux[csvMapper.type, Repr, OutRepr],
toMap: ToMap.Aux[OutRepr, Symbol, String]
): List[Map[String, String]] = {
def transform(row: Row): Map[String, String] = {
val formattedRows = labelledGeneric
.to(row)
.map(csvMapper)
val fields = toMap(formattedRows)
.map {
case (k: Symbol, value: String) =>
(k.toString -> value)
}
fields
}
rows.map(transform(_))
}
}
When I try to use with simple case class:
final case class ProjectCsvRow(
project: Option[String],
something: Option[String],
site: Option[String],
state: Option[Int]
)
new CsvBuilder[ProjectCsvRow](
List(ProjectCsvRow(Some(""), None, None, None))
).build
I'm getting
could not find implicit value for parameter mapper: shapeless.ops.hlist.Mapper.Aux[stabilizer$1.csvMapper.type,Repr,OutRepr]
I think I'm missing something, but I can't really figure it out.
I already checked if there's instance of CsvEncoder for every field.
Firstly, your Poly is incorrect. Type Symbol is too rough. Keys of a record are not just Symbols, they are singleton subtypes of Symbol. You should define
object csvMapper extends Poly1 {
implicit def caseEncode[K <: Symbol, V](
implicit e: CsvEncoder[V]
): Case.Aux[FieldType[K, V], FieldType[K, String]] =
at[FieldType[K, V]](s => field[K](e.encode(s)))
}
Secondly, making a Poly nested into a class (rather than object) can be dangerous (it starts to depend on an instance of CsvBuilder and therefore on type Row). So move csvMapper outside CsvBuilder (so that its path is stable).

Achieving Ad hoc polymorphism at function parameter level (mixing parameters of different type)

When I have a function in Scala:
def toString[T: Show](xs: T*): String = paths.map(_.show).mkString
And the following type class instances in scope:
implicit val showA: Show[MyTypeA]
implicit val showB: Show[MyTypeB]
I can use function toString in the following ways:
val a1: MyTypeA
val a2: MyTypeA
val stringA = toString(a1, a2)
val b1: MyTypeB
val b2: MyTypeB
val stringB = toString(b1, b2)
But I cannot call toString mixing parameters of type MyTypeA and MyTypeB:
// doesn't compile, T is inferred to be of type Any
toString(a1, b1)
Is it possible to redefine toString in such a way that it becomes possible to mix parameters of different types (but only for which a Show typeclass is available)?
Note that I am aware of the cats show interpolator which solves this specific example, but I'm looking for a solution which can be applied to different cases as well (e.g. toNumber).
I am also aware of circumventing the problem by calling .show on the parameters before passing them to the toString function, but I'm looking for a way to avoid this as it results in code duplication.
Example with shapeless:
object myToString extends ProductArgs { //ProductArgs allows changing variable number of arguments to HList
//polymorphic function to iterate over values of HList and change to a string using Show instances
object showMapper extends Poly1 {
implicit def caseShow[V](implicit show: Show[V]): Case.Aux[V, String] = {
at[V](v => show.show(v))
}
}
def applyProduct[ARepr <: HList](
l: ARepr
)(
implicit mapper: Mapper[showMapper.type, ARepr]
): String = l.map(showMapper).mkString("", "", "")
}
Now let's test it:
case class Test1(value: String)
case class Test2(value: String)
case class Test3(value: String)
implicit val show1: Show[Test1] = Show.show(_.value)
implicit val show2: Show[Test2] = Show.show(_.value)
println(myToString(Test1("a"), Test2("b"))) //"ab"
println(myToString(Test1("a"), Test2("b"), Test3("c"))) //won't compile since there's no instance of Show for Test3
By the way, I think toString is not the best name, because probably it can cause weird conflicts with toString from java.lang.Object.
If you don't want to mess with shapeless, another solution that comes to my mind is to just create functions with different arity:
def toString[A: Show](a: A): String = ???
def toString[A: Show, B: Show](a: A, b: B): String = ???
//etc
It's definitely cumbersome, but it might be the easiest way to solve your problem.
Here's one way to do it in Dotty (note that most of the Dotty-specific features used here are not necessary; they're just to make life easier, but being able to abstract over tuples of different arities is something you can't do (easily) in Scala 2):
opaque type Show[T] = T => String
opaque type ShowTuple[T <: Tuple] = T => String
object ShowTuple {
given ShowTuple[EmptyTuple] = _ => ""
given showTuple[H, T <: Tuple](using show: Show[H], showTail: ShowTuple[T]) as ShowTuple[H *: T] =
{ case h *: t => show(h) + "," + showTail(t) }
}
def multiToString[T <: Tuple](t: T)(using showTuple: ShowTuple[T]) =
showTuple(t)
It can be used like this:
class TypeA(val i: Int)
class TypeB(val s: String)
class TypeC(val b: Boolean)
given Show[TypeA] = t => s"TypeA(${t.i})"
given Show[TypeB] = t => s"TypeB(${t.s})"
given Show[TypeC] = t => s"TypeC(${t.b})"
println(multiToString((new TypeA(10), new TypeB("foo"), new TypeC(true))))
Using a type for which an implicit is not given fails:
class TypeD
multiToString((new TypeA(10), new TypeB("foo"), new TypeC(true), new TypeD))
Try it in Scastie
What is the type of paths?
If it's List[T] then there should be an implicit Show[T] in scope.
If it's List[Any] then there should be an implicit Show[Any] in scope.
If paths contains elements of different types and paths is not a List[Any] then paths shouldn't be a List[...] at all. It can be of type L <: HList. You can try
import shapeless.{HList, HNil, Poly1, Poly2}
import shapeless.ops.hlist.{LeftReducer, Mapper}
trait Show[T] {
def show(t: T): String
}
implicit class ShowOps[T](t: T) {
def show(implicit s: Show[T]): String = s.show(t)
}
object show extends Poly1 {
implicit def cse[T: Show]: Case.Aux[T, String] = at(_.show)
}
object concat extends Poly2 {
implicit def cse: Case.Aux[String, String, String] = at(_ + _)
}
def toString[L <: HList, L1 <: HList](xs: L)(implicit
mapper: Mapper.Aux[show.type, L, L1],
reducer: LeftReducer.Aux[L1, concat.type, String]
): String = xs.map(show).reduceLeft(concat)
type MyTypeA
type MyTypeB
implicit val showA: Show[MyTypeA] = ???
implicit val showB: Show[MyTypeB] = ???
val a1: MyTypeA = ???
val b1: MyTypeB = ???
toString(a1 :: b1 :: HNil)

How does HList.foldRight look for implicits when used in the implementation of a type class?

I am newbie at using Shapeless, and I am experimenting with Shapeless for automatic type class generation and folding over HLists.
My goal is to render a HList as (a, b, c, d) using a typeclass implementation of scalaz.Show
My first step was to experiment in the REPL with the following code
import shapeless._
import shapeless.ops.hlist._
object prettyPrint extends Poly2 {
implicit def defaultCase[A] = at((a:A, z:String)=>s", ${a.toString}$z")
}
def print[H, T<:HList](f: H :: T)(implicit folder:RightFolder.Aux[H :: T, String, prettyPrint.type, String]) = {
f.foldRight("")(prettyPrint)
}
val f = 1::'a::2::'b::HNil
val res = s"(${f.head}${print(f.tail)})" // Results res: String = (1, 'a, 2, 'b)
After this I implemented the following method in my implementation of LabelledTypeClassCompanion[...]. Unfortunately, this code does not compile because the compiler is complaining about missing implicits, even though I can't tell what the difference is between the code in the REPL and the code below. My Question is what is the problem in the code below and how can I fix it?
def showFold[H, T<:HList](f: H::T)(implicit folder:RightFolder.Aux[ H::T, String, prettyPrint.type, String]) = {
f.foldRight("")(prettyPrint)
}
override def product[H, T <: HList](name: String, ch: ScalazShow[H], ct: ScalazShow[T]): ScalazShow[H :: T] = {
new ScalazShow[H :: T] {
override def shows(ft: (H :: T)): String = {
showFold(ft) // This does not compile
}
}
}
Error:(49, 18) could not find implicit value for parameter folder: shapeless.ops.hlist.RightFolder.Aux[shapeless.::[H,T],String,com.fpinscala.ninetynine.prettyPrint.type,String]
showFold(ft) // This does not compile
Below is the complete implementation
package com.fpinscala.ninetynine
import shapeless._
import shapeless.ops.hlist.RightFolder
import scalaz.{Show => ScalazShow}
object prettyPrint extends Poly2 {
implicit def defaultCase[A]:this.Case.Aux[A, String, String] = at[A, String]{
(a,z) => s", $a$z"
}
}
object ShowImpl extends LabelledTypeClassCompanion[ScalazShow] {
implicit def symbolShow : ScalazShow[Symbol] = new ScalazShow[Symbol] {
override def shows(f: Symbol): String = f.toString()
}
implicit def intShow : ScalazShow[Int] = new ScalazShow[Int] {
override def shows(f: Int): String = f.toString
}
override val typeClass: LabelledTypeClass[ScalazShow] = new LabelledTypeClass[ScalazShow] {
override def coproduct[L, R <: Coproduct](name: String, cl: => ScalazShow[L], cr: => ScalazShow[R]): ScalazShow[L :+: R] = new ScalazShow[L :+: R] {
override def shows(lr: (L :+: R)): String = lr match {
case Inl(l) => cl.shows(l)
case Inr(r) => cr.shows(r)
}
}
override def emptyCoproduct: ScalazShow[CNil] = new ScalazShow[CNil] {
override def shows(f: CNil): String = ""
}
def showFold[H, T<:HList](f: H::T)(implicit folder:RightFolder.Aux[ H::T, String, prettyPrint.type, String]) = {
f.foldRight("")(prettyPrint)
}
override def product[H, T <: HList](name: String, ch: ScalazShow[H], ct: ScalazShow[T]): ScalazShow[H :: T] = {
new ScalazShow[H :: T] {
override def shows(ft: (H :: T)): String = {
showFold(ft) // This does not compile
}
}
}
override def project[F, G](instance: => ScalazShow[G], to: (F) => G, from: (G) => F): ScalazShow[F] = new ScalazShow[F] {
override def shows(f: F): String = instance.shows(to(f))
}
override def emptyProduct: ScalazShow[HNil] = new ScalazShow[HNil] {
override def shows(f: HNil): String = ""
}
}
}
You can think of a type class constraint as a way to carry some information about a type from a concrete context to a generic context (moving backward through the call stack). In this case you actually really do need that RightFolder instance if you want to write your implementation this way, but the method signatures in LabelledTypeClass don't allow you to carry that information through, so you're out of luck (the basic idea is possible, though—you just need a slightly different approach).
Update
I just realized I misread your question slightly—because you were using the TypeClass type class I assumed you wanted instances for case classes and sealed trait hierarchies as well as hlists and coproducts. My answer gives you all of these (just like TypeClass would), so you can write this:
scala> (123 :: "abc" :: HNil).shows
res2: String = (123, abc)
As well as the case class and sealed trait examples I give below. If you don't want case classes and sealed traits you can just remove the genericShow definition.
Why the difference between concrete and generic contexts
Here's a simpler case to start with. Suppose we want to use Show to print a value twice. We can do something like this:
scala> import scalaz._, Scalaz._
import scalaz._
import Scalaz._
scala> val x: Int = 123
x: Int = 123
scala> s"${ x.shows }${ x.shows }"
res0: String = 123123
Here x has a concrete type, and when we call .shows on it, the compiler will try to find an instance of Show for that concrete type. Scalaz provides a Show[Int], so everything works just fine and we get the result we want.
Next we can try writing a generic version:
def toStringTwice[X](x: X): String = s"${ x.shows }${ x.shows }"
But the compiler will complain:
<console>:18: error: value shows is not a member of type parameter X
def toStringTwice[X](x: X): String = s"${ x.shows }${ x.shows }"
^
This is because the compiler can't prove that X has a Show instance, since it doesn't know anything at all about X. You could just write a bunch of overloaded concrete methods:
scala> def toStringTwice(x: String): String = s"${ x.shows }${ x.shows }"
toStringTwice: (x: String)String
scala> def toStringTwice(x: Int): String = s"${ x.shows }${ x.shows }"
toStringTwice: (x: Int)String
...
But this is exactly the kind of annoying boilerplate that type classes are designed to save you from. Instead of enumerating all the types you have Show instances for, you can abstract over them by providing the compiler with exactly as much information as it needs:
scala> def toStringTwice[X: Show](x: X): String = s"${ x.shows }${ x.shows }"
toStringTwice: [X](x: X)(implicit evidence$1: scalaz.Show[X])String
Now you can call it with an Int, or anything else that has a Show instance:
scala> toStringTwice(123)
res2: String = 123123
What you can't do is call it with another unconstrained generic type:
def toStringFourTimes[X](x: X): String = s"${ toStringTwice(x) * 2 }"
Instead you have to add the constraint again:
scala> def toStringFourTimes[X: Show](x: X): String = s"${ toStringTwice(x) * 2 }"
toStringFourTimes: [X](x: X)(implicit evidence$1: scalaz.Show[X])String
And so on—you have to carry along the Show constraint all the way until you've got a concrete type. You can only use toStringTwice in two ways: on a concrete type that has a Show instance, or on a generic type that has a Show constraint.
Note that none of the above is Shapeless-specific—this is simply the way type classes work.
One possible fix
Unfortunately this doesn't seem to me like a very good use case for LabelledTypeClass, since the desired instance doesn't really fit the way of building up instances that the TypeClass type classes support. You could probably do it but I don't really want to try.
There's also an issue in the way your prettyPrint works—it's not actually using the Show instance for A (there's not even one to use), but is instead calling the horrible universal toString.
Here's a quick first draft of how I'd probably write this:
import scalaz.Show, scalaz.Scalaz._
import shapeless._
import shapeless.ops.coproduct.Folder
import shapeless.ops.hlist.RightReducer
object prettyPrint2 extends Poly2 {
implicit def defaultCase[A: Show]: Case.Aux[A, String, String] =
at[A, String]((a, z) => s"$a, $z")
}
object prettyPrint extends Poly1 {
implicit def defaultCase[A: Show]: Case.Aux[A, String] = at[A](_.shows)
}
implicit def hlistShow[L <: HList](implicit
reducer: RightReducer.Aux[L, prettyPrint2.type, String]
): Show[L] = Show.shows(l => "(" + l.reduceRight(prettyPrint2) + ")")
implicit def coproductShow[C <: Coproduct](implicit
folder: Folder.Aux[prettyPrint.type, C, String]
): Show[C] = Show.shows(_.fold(prettyPrint))
implicit def genericShow[A, R](implicit
gen: Generic.Aux[A, R],
reprShow: Show[R]
): Show[A] = reprShow.contramap(gen.to)
And then:
scala> Foo(123, "abc").shows
res0: String = (123, abc)
scala> (Foo(123, "abc"): Base).shows
res1: String = (123, abc)
You may run into corner cases involving nested case classes, etc. that don't work because of compiler bugs (see my slides here about generic derivation in Scala for some details), but this approach should more or less do what you want.

How to shapeless case classes with attributes and typeclasses?

I am currently implementing a library to serialize and deserialize to and from XML-RPC messages. It's almost done but now I am trying to remove the boilerplate of my current asProduct method using Shapeless. My current code:
trait Serializer[T] {
def serialize(value: T): NodeSeq
}
trait Deserializer[T] {
type Deserialized[T] = Validation[AnyErrors, T]
type AnyErrors = NonEmptyList[AnyError]
def deserialize(from: NodeSeq): Deserialized[T]
}
trait Datatype[T] extends Serializer[T] with Deserializer[T]
// Example of asProduct, there are 20 more methods like this, from arity 1 to 22
def asProduct2[S, T1: Datatype, T2: Datatype](apply: (T1, T2) => S)(unapply: S => Product2[T1, T2]) = new Datatype[S] {
override def serialize(value: S): NodeSeq = {
val params = unapply(value)
val b = toXmlrpc(params._1) ++ toXmlrpc(params._2)
b.theSeq
}
// Using scalaz
override def deserialize(from: NodeSeq): Deserialized[S] = (
fromXmlrpc[T1](from(0)) |#| fromXmlrpc[T2](from(1))
) {apply}
}
My goal is to allow the user of my library to serialize/deserialize case classes without force him to write boilerplate code. Currently, you have to declare the case class and an implicit val using the aforementioned asProduct method to have a Datatype instance in context. This implicit is used in the following code:
def toXmlrpc[T](datatype: T)(implicit serializer: Serializer[T]): NodeSeq =
serializer.serialize(datatype)
def fromXmlrpc[T](value: NodeSeq)(implicit deserializer: Deserializer[T]): Deserialized[T] =
deserializer.deserialize(value)
This is the classic strategy of serializing and deserializing using type classes.
At this moment, I have grasped how to convert from case classes to HList via Generic or LabelledGeneric. The problem is once I have this conversion done how I can call the methods fromXmlrpc and toXmlrpc as in the asProduct2 example. I don't have any information about the types of the attributes in the case class and, therefore, the compiler cannot find any implicit that satisfy fromXmlrpc and toXmlrpc. I need a way to constrain that all the elements of a HList have an implicit Datatype in context.
As I am a beginner with Shapeless, I would like to know what's the best way of getting this functionality. I have some insights but I definitely have no idea of how to get it done using Shapeless. The ideal would be to have a way to get the type from a given attribute of the case class and pass this type explicitly to fromXmlrpc and toXmlrpc. I imagine that this is not how it can be done.
First, you need to write generic serializers for HList. That is, you need to specify how to serialize H :: T and HNil:
implicit def hconsDatatype[H, T <: HList](implicit hd: Datatype[H],
td: Datatype[T]): Datatype[H :: T] =
new Datatype[H :: T] {
override def serialize(value: H :: T): NodeSeq = value match {
case h :: t =>
val sh = hd.serialize(h)
val st = td.serialize(t)
(sh ++ st).theSeq
}
override def deserialize(from: NodeSeq): Deserialized[H :: T] =
(hd.deserialize(from.head) |#| td.deserialize(from.tail)) {
(h, t) => h :: t
}
}
implicit val hnilDatatype: Datatype[HNil] =
new Datatype[HNil] {
override def serialize(value: HNil): NodeSeq = NodeSeq()
override def deserialize(from: NodeSeq): Deserialized[HNil] =
Success(HNil)
}
Then you can define a generic serializer for any type which can be deconstructed via Generic:
implicit def genericDatatype[T, R](implicit gen: Generic.Aux[T, R],
rd: Lazy[Datatype[R]]): Datatype[T] =
new Datatype[T] {
override def serialize(value: T): NodeSeq =
rd.value.serialize(gen.to(value))
override def deserialize(from: NodeSeq): Deserialized[T] =
rd.value.deserialize(from).map(rd.from)
}
Note that I had to use Lazy because otherwise this code would break the implicit resolution process if you have nested case classes. If you get "diverging implicit expansion" errors, you could try adding Lazy to implicit parameters in hconsDatatype and hnilDatatype as well.
This works because Generic.Aux[T, R] links the arbitrary product-like type T and a HList type R. For example, for this case class
case class A(x: Int, y: String)
shapeless will generate a Generic instance of type
Generic.Aux[A, Int :: String :: HNil]
Consequently, you can delegate the serialization to recursively defined Datatypes for HList, converting the data to HList with Generic first. Deserialization works similarly but in reverse - first the serialized form is read to HList and then this HList is converted to the actual data with Generic.
It is possible that I made several mistakes in the usage of NodeSeq API above but I guess it conveys the general idea.
If you want to use LabelledGeneric, the code would become slightly more complex, and even more so if you want to handle sealed trait hierarchies which are represented with Coproducts.
I'm using shapeless to provide generic serialization mechanism in my library, picopickle. I'm not aware of any other library which does this with shapeless. You can try and find some examples how shapeless could be used in this library, but the code there is somewhat complex. There is also an example among shapeless examples, namely S-expressions.
Vladimir's answer is great and should be the accepted one, but it's also possible to do this a little more nicely with Shapeless's TypeClass machinery. Given the following setup:
import scala.xml.NodeSeq
import scalaz._, Scalaz._
trait Serializer[T] {
def serialize(value: T): NodeSeq
}
trait Deserializer[T] {
type Deserialized[T] = Validation[AnyErrors, T]
type AnyError = Throwable
type AnyErrors = NonEmptyList[AnyError]
def deserialize(from: NodeSeq): Deserialized[T]
}
trait Datatype[T] extends Serializer[T] with Deserializer[T]
We can write this:
import shapeless._
object Datatype extends ProductTypeClassCompanion[Datatype] {
object typeClass extends ProductTypeClass[Datatype] {
def emptyProduct: Datatype[HNil] = new Datatype[HNil] {
def serialize(value: HNil): NodeSeq = Nil
def deserialize(from: NodeSeq): Deserialized[HNil] = HNil.successNel
}
def product[H, T <: HList](
dh: Datatype[H],
dt: Datatype[T]
): Datatype[H :: T] = new Datatype[H :: T] {
def serialize(value: H :: T): NodeSeq =
dh.serialize(value.head) ++ dt.serialize(value.tail)
def deserialize(from: NodeSeq): Deserialized[H :: T] =
(dh.deserialize(from.head) |#| dt.deserialize(from.tail))(_ :: _)
}
def project[F, G](
instance: => Datatype[G],
to: F => G,
from: G => F
): Datatype[F] = new Datatype[F] {
def serialize(value: F): NodeSeq = instance.serialize(to(value))
def deserialize(nodes: NodeSeq): Deserialized[F] =
instance.deserialize(nodes).map(from)
}
}
}
Be sure to define these all together so they'll be properly companioned.
Then if we have a case class:
case class Foo(bar: String, baz: String)
And instances for the types of the case class members (in this case just String):
implicit object DatatypeString extends Datatype[String] {
def serialize(value: String) = <s>{value}</s>
def deserialize(from: NodeSeq) = from match {
case <s>{value}</s> => value.text.successNel
case _ => new RuntimeException("Bad string XML").failureNel
}
}
We automatically get a derived instance for Foo:
scala> case class Foo(bar: String, baz: String)
defined class Foo
scala> val fooDatatype = implicitly[Datatype[Foo]]
fooDatatype: Datatype[Foo] = Datatype$typeClass$$anon$3#2e84026b
scala> val xml = fooDatatype.serialize(Foo("AAA", "zzz"))
xml: scala.xml.NodeSeq = NodeSeq(<s>AAA</s>, <s>zzz</s>)
scala> fooDatatype.deserialize(xml)
res1: fooDatatype.Deserialized[Foo] = Success(Foo(AAA,zzz))
This works about the same as Vladimir's solution, but it lets Shapeless abstract some of the boring boilerplate of type class instance derivation so you don't have to get your hands dirty with Generic.