Reputation: 5037
Having this type class for converting a Map into a case class:
/**
* Type class for transforming a map of values into a case class instance.
*
* @tparam T Wanted case class type.
*/
@implicitNotFound("Missing ToCaseClassMapper implementation for type ${T}")
trait ToCaseClassMapper[T <: Product] {
def toCaseClass(map: Map[String, Any]): T
}
And this function to implicitly get the correct mapper
def toCaseClass[T <: Product](map: Map[String, Any])(implicit mapper: ToCaseClassMapper[T]): T = {
mapper.toCaseClass(map)
}
It can be used as toCaseClass[User](aUserMap) // returns User
But I also would like to be able to use this function with an Option[Map[]] or Future[Map[]] or List[Map[]]. So I implemented a generic function using a functor like this:
def toCaseClass[T <: Product, F[_]: cats.Functor](fOfMaps: F[Map[String, Any]])(implicit mapper: ToCaseClassMapper[T]): F[T] = {
cats.Functor[F].map(fOfMaps)(map => toCaseClass(map)(mapper))
}
But now this function has to be used as toCaseClass[User,List](listOfUserMaps) // returns List[User]
.
However, I'd would like to be able to use the function as
toCaseClass[User](listOfMaps)
toCaseClass[User](futureOfMap)
toCaseClass[User](optionOfMap)
without the need to specify the functor type.
Is this somehow possible?
Could Shapeless's Lazy be used to solve this?
Edit: solution
Thanks to @Jasper-m and @dk14 for their answers.
So the 'trick' to solve this is to capture the type 'T' first in a class before the Functor type. I liked @Jasper-m solution with the 'apply' method since that would keep the syntax almost similar to what it was before.
I made a few adjustments though. Since there was already the 'ToCaseClassMapper' class which also captures the type 'T', I decided to combine it with the 'ToCaseClass' class. Also, with @Jasper-m's approach, when using the 'toCaseClass' function when mapping over some value like Option(value).map(toCaseClass)
the usage of toCaseClass
had to be different for when the value was a Map or a List[Map].
My solution is now as follows:
@implicitNotFound("Missing ToCaseClassMapper implementation for type ${T}")
trait ToCaseClassMapper[T <: Product] {
def toCaseClass(map: Map[String, Any]): T
import scala.language.higherKinds
def toCaseClass[F[_]: cats.Functor, A](fOfMaps: F[Map[String, A]]): F[T] = {
cats.Functor[F].map(fOfMaps)(toCaseClass)
}
}
Since the ToCaseClassMapper
instance was already implicitly available where the toCaseClass
function was used, I decided to throw away that function and just replace it with mapper.toCaseClass(_)
. This cleaned up some unneeded code and now the syntax for using the mapper is the same regardless whether the value is a Map or Option, List, Future (or any other Functor).
Upvotes: 2
Views: 409
Reputation: 15086
Currently it's not possible in Scala to have one type parameter provided explicitly and another one in the same type parameter list be inferred, nor is it currently possible to have multiple type parameter lists for a method. A workaround is to create a helper class and split your method call in two stages: first create an instance of the helper class, then call the apply
method on that object.
class ToCaseClass[T <: Product] {
def apply[F[_]: cats.Functor, A](fOfMaps: F[Map[String, A]])(implicit mapper: ToCaseClassMapper[T]): F[T] = {
cats.Functor[F].map(fOfMaps)(map => toCaseClass(map)(mapper))
}
}
def toCaseClass[T <: Product] = new ToCaseClass[T]
def toCaseClass[T <: Product](map: Map[String, Any])(implicit mapper: ToCaseClassMapper[T]): T = {
mapper.toCaseClass(map)
}
toCaseClass[User](listOfMaps)
toCaseClass[User](futureOfMap)
toCaseClass[User](optionOfMap)
Edit: As pointed out by dk14, there is still a type inference problem here, where F
is inferred as Any
. I don't know what causes it, but I think it is a separate orthogonal problem from the one being solved by this pattern.
Edit 2: I figured it out. It's because F
is invariant in its type parameter. F[Map[String, String]]
is not a subtype of F[Map[String, Any]]
, so the compiler does something strange and infers F
as Any
. A solution is to put a type parameter A
instead of Any
, or use an existential type Map[String,_]
.
Upvotes: 1
Reputation: 22374
This works:
class Mapper[T <: Product](implicit val mapper: ToCaseClassMapper[T]){
def toCaseClass[F[_]: cats.Functor, Z <: Map[String, Any]](fOfMaps: F[Z]): F[T] = {
cats.Functor[F].map(fOfMaps)(map => mapper.toCaseClass(map))
}
}
object Mapper{
def apply[T <: Product: ToCaseClassMapper] = new Mapper[T]{}
}
import cats.implicits._
Mapper[User].toCaseClass(List(Map("aaa" -> 0)))
Few tricks besides obvious introducing class (to split type parameters) were used as well:
1) move mapper
to constructor so it could be resolved first (not sure it helped)
2) what definitely helped is to introduce Z <: Map[String, Any]
, otherwise scala (at least my old version 2.11.8) would infer F[_]
as Any
for some reason
P.S. You can use apply
instead of toCaseClass
as well - it would shorten the syntax
Upvotes: 1