Using shapeless, one can use LabelledGeneric to update case class fields like so:
case class Test(id: Option[Long], name: String)
val test = Test(None, "Name")
val gen = LabelledGeneric[Test]
scala> gen.from(gen.to(test) + ('id ->> Option(1L)))
res0: Test = Test(Some(1),Name)
I would like the Test class (and others) to extend an abstract class Model, that will implement a method withId that would use a LabelledGeneric similar to the above code to update the id field, should it have one (which it should).
My attempt adds an implicit parameter of a LabelledGeneric[A] to the constructor of Model, which materializes just fine. I also need to somehow provide evidence to the record syntax that the LabelledGeneric#Repr has the id field to replace. Adding an implicit Updater parameter to withId satisfies the compiler, so that the code below will compile, but it is not usable.
import shapeless._, record._, ops.record._, labelled._, syntax.singleton._, tag._
abstract class Model[A](implicit gen: LabelledGeneric[A] { type Repr <: HList }) { this: A =>
def id: Option[Long]
val idWitness = Witness("id")
type F = FieldType[Symbol with Tagged[idWitness.T], Option[Long]]
def withId(id: Long)(implicit u: Updater.Aux[gen.Repr, F, gen.Repr]) =
gen.from(gen.to(this) + ('id ->> Option(id)))
}
case class Test(id: Option[Long], name: String) extends Model[Test]
When calling test.withId(1), the implicit Updater fails to materialize. The macro reports that gen.Repr isn't an HList type, when it in fact is. It seems that this match is the one that fails, where u baseType HConsSym returns <notype>. Equivalent to:
scala> weakTypeOf[test.gen.Repr].baseType(weakTypeOf[::[_, _]].typeConstructor.typeSymbol)
res12: reflect.runtime.universe.Type = <notype>
This is using shapeless 2.3, though it fails for different reasons in 2.2 (seems as though Updater had a large refactor).
Is it possible to accomplish this with shapeless, or am I way off target?
The main issue here is that the refined result type of the LabelledGeneric (Repr) is lost. At Model, the only thing known about Repr is Repr <: HList. The implicit Updater.Aux[gen.Repr, F, gen.Repr] searches for something that is only known as _ <: HList and thus fails to materialize.
You'd have to define Model with two type parameters
abstract class Model[A, L <: HList](implicit gen: LabelledGeneric.Aux[A, L])
but this doesn't allow you to write class Test extends Model[Test] and you have to write the labelled generic type by hand.
If you instead move the gen down to withId, you can make it work:
object Model {
private type IdField = Symbol with Tagged[Witness.`"id"`.T]
private val IdField = field[IdField]
type F = FieldType[IdField, Option[Long]]
}
abstract class Model[A] { this: A =>
import Model._
def id: Option[Long]
def withId[L <: HList](id: Long)(implicit // L captures the fully refined `Repr`
gen: LabelledGeneric.Aux[A, L], // <- in here ^
upd: Updater.Aux[L, F, L] // And can be used for the Updater
): A = {
val idf = IdField(Option(id))
gen.from(upd(gen.to(this), idf))
}
}
case class Test(id: Option[Long], name: String) extends Model[Test]
If you're concerned with resolution performance, you can cache the value(s) in the companion of Test:
case class Test(id: Option[Long], name: String) extends Model[Test]
object Test {
implicit val gen = LabelledGeneric[Test]
}
This would mean that code like this
val test = Test(None, "Name")
println("test.withId(12) = " + test.withId(12))
println("test.withId(12).withId(42) = " + test.withId(12).withId(42))
would use the definition of Test.gen instead of materializing a new LabelledGeneric every time.
This works for both, shapeless 2.2.x and 2.3.x.
Related
I have some code:
case class Name(name: String) extends StaticAnnotation
case class Example(#Name("baz") foo: String, bar: Int)
object nameAnnotationZip2 extends Poly2 {
implicit val newName: Case.Aux[Symbol, Some[Name], String] =
at((_, name) => name.value.name)
implicit val existingName: Case.Aux[Symbol, None.type, String] =
at((field, _) => field.name)
}
def renameRecord[
A,
Record <: HList,
Fields <: HList,
Annotations <: HList,
ZippedWith <: HList
](a: A)(implicit
generic: LabelledGeneric.Aux[A, Record],
fields: Keys.Aux[Record, Fields],
annotations: Annotations.Aux[Name, A, Annotations],
zippedWith: ZipWith.Aux[
Fields,
Annotations,
nameAnnotationZip2.type,
ZippedWith
]
) = {
val record = generic.to(a)
println(fields())
println(annotations())
zippedWith(fields(), annotations())
}
renameRecord(Example("qix", 5))
which attempts to create a record from an instance with renamed keys based on an annotation. unfortunately it always gives me a
could not find implicit value for parameter zippedWith: shapeless.ops.hlist.ZipWith.Aux[Fields,Annotations,nameAnnotationZip2.type,ZippedWith]
I haven't added the ZipWithKeys bit yet, but I believe I need to work around this issue first.
I believe I'm simply using Poly totally wrong somehow, but the compiler message isn't helping me find my error.
edit/part two:
I can work around by using Zip instead of ZipWith, converting to Sized, simply mapping the seq without Poly, and converting back to HList. what's the runtime performance difference between doing that and the above?
The type of Fields isn't Symbol, it's Symbol ## "foo". Case's generic parameters aren't variant so nameAnnotationZip2 isn't defined for the elements of Fields.
Adding a generic parameter to the cases fixes the issue
object nameAnnotationZip2 extends Poly2 {
implicit def newName[K <: Symbol]: Case.Aux[K, Some[Name], String] =
at((_, name) => name.value.name)
implicit def existingName[K <: Symbol]: Case.Aux[K, None.type, String] =
at((field, _) => field.name)
}
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)
}
Is there a way to derive a type from an existing one in Scala?
For example, for case class Person(name: String, age: Int) I'd like to get a Product/Tuple of (Option[String], Option[Int]), i.e. a type mapped from an existing one.
There's a feature in Typescript (mapped types) that allows this relatively easily, which is how I started thinking down this path. But I'm not sure how something like this would be done in Scala.
I feel like the solution involves using shapeless in some way but I'm not sure how to get there.
I'd suggest parameterize the type as follows:
case class Person[F[_]](name: F[String], age: F[Int])
And then you can derive types you want, like
import cats.Id
type IdPerson = Person[Id]
type OptPerson = Person[Option]
Where cats.Id is simply defined as type Id[A] = A. It is straightforward to write your own, but I suggest using cats' one since it comes with useful typeclass instances.
With Shapeless you can define type class
import shapeless.ops.{hlist, product, tuple}
import shapeless.poly.~>
import shapeless.{Generic, HList, Id, the}
trait Partial[A] {
type Out
}
object Partial {
type Aux[A, Out0] = Partial[A] { type Out = Out0 }
object optionPoly extends (Id ~> Option) {
override def apply[T](t: T): Option[T] = null
}
// implicit def mkPartial[A, L <: HList, L1 <: HList](implicit
// generic: Generic.Aux[A, L],
// mapper: hlist.Mapper.Aux[optionPoly.type, L, L1],
// tupler: hlist.Tupler[L1]): Aux[A, tupler.Out] = null
implicit def mkPartial[A, T](implicit
toTuple: product.ToTuple.Aux[A, T],
mapper: tuple.Mapper[T, optionPoly.type],
): Aux[A, mapper.Out] = null
}
and use it (the is improved version of implicitly)
case class Person(name: String, age: Int)
// val pp = the[Partial[Person]]
// type PersonPartial = pp.Out
type PersonPartial = the.`Partial[Person]`.Out
implicitly[PersonPartial =:= (Option[String], Option[Int])]
Consider the following:
sealed trait baseData {
def weight: Int
def priority: Int
}
case class data1(override val weight: Int, override val priority: Int) extends baseData
How would I define a function with the following signature that transforms data1 into an HList?
def toHlist[A <: baseData] (data: A) = {}
I want to pass in a trait instance into the toHlist function instead of the actual case class because there will be more than one case class extending the trait. I also don't want to hardcode any fields; I'm looking for a totally generic solution.
I'm sure this is doable with the Shapeless library, but haven't been able to figure out how.
EDIT
toHList needs to be able to handle a baseData pointer to a case class instance, as so:
val data: baseData = data1(1,2)
toHlist(data)
The reason for this is that we will have more than one case class extending baseData, and we will now know which one to pass to toHlist until run time.
This probably isn't the final answer, but it will help you refine your question,
scala> def toHlist[A <: baseData](data: A)(implicit gen: Generic[A]): gen.Repr = gen.to(data)
toHlist: [A <: baseData](data: A)(implicit gen: shapeless.Generic[A])gen.Repr
scala> toHlist(data1(23, 13))
res0: shapeless.::[Int,shapeless.::[Int,shapeless.HNil]] = 23 :: 13 :: HNil
I'm fairly new to Shapeless, as one will infer from my question. Given an instance of LabelledGeneric, how do I get the name of the class that it represents. I can get the field name information from Keys, so I assume I need some other kind of Witness that encapsulates the type itself, but I cannot figure out which.
Eg, if I have a case class called Foo in package com.bar, I want to get the string "com.bar.Foo" (or separately is fine).
implicit def example[T, Repr <: HList](implicit label: LabelledGeneric.Aux[T, Repr],
kk: Keys[Repr]): Bibble[T] = new Bibble[T] {
override def typeName(value: T): String = ???
}
Shapeless's Generic provides a sum-of-products representation for case classes and sealed traits, which means that if we have a simple ADT like this:
sealed trait Base
case object Foo extends Base
case class Bar(i: Int, s: String) extends Base
Then Generic[Base] will give us a mapping to a Foo.type :+: Bar :+: CNil—i.e. a Foo.type or a Bar (where the or means we're talking about a "sum type" in type theoretic terms), and a Generic[Bar] gives us a mapping to an Int :: String :: HNil, which is Int and a String (a product type, where "product" has roughly the same meaning that it does in the case of the scala.ProductN types in the standard library).
LabelledGeneric uses an enhanced version of the sum-of-products representation, where each of the terms in the product or sum is tagged with a label. In the case of a sealed trait, these will be the constructor names for each subtype, and in the case of a case class they'll be the member names. These aren't fully-qualified names—just labels that disambiguate locally.
Generic and LabelledGeneric aren't intended to serve as general-purpose tools for compile-time reflection. They're not available for arbitrary types, for example, and they don't provide access to the name of the type itself.
Your best bet is probably to use TypeTag, but if you want a type-level representation of the name (like LabelledGeneric provides for labels), you'll need to define your own type class with macro-generated instances. Something like the following should work:
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context
trait TypeInfo[A] { type Name <: String; def name: String }
object TypeInfo {
type Aux[A, Name0 <: String] = TypeInfo[A] { type Name = Name0 }
def apply[A](implicit ti: TypeInfo[A]): Aux[A, ti.Name] = ti
implicit def materializeTypeInfo[A, Name <: String]: Aux[A, Name] =
macro matTypeInfoImpl[A, Name]
def matTypeInfoImpl[A: c.WeakTypeTag, Name <: String](c: Context): c.Tree = {
import c.universe._
val A = c.weakTypeOf[A]
val name = A.typeSymbol.name.decodedName.toString.trim
q"new TypeInfo[$A] { type Name = ${ Constant(name) }; def name = $name }"
}
}
This is probably overkill for your use case, though, if you only need value-level strings.