简体   繁体   中英

Scala type class pattern and generic methods

I'm trying to write a generic extractor for parsing json POST body using spray and spray-json.

However I'm struggling to get it working with more than one model. Here's the case statement in the service object:

import MyJsonProtocol._

...

def receive = {
  case Post (Routes.person.post, p: Person) => sender ! Ok(Actions.person.post(p))
  case Get  (Routes.foo.forId(x))           => sender ! Ok(x)
  case _                                    => sender ! Ok("No handler")
}

And here's the extractor I wrote (that works as long as there's only a JsonReader for a single model in the scope of the case statement):

//NB. Json.parse returns an Option[T]
object Post extends Request {
  def unapply[T:JsonReader](req: HttpRequest): Option[(String, T)] = req match {
    case HttpRequest(POST, url, _, HttpBody(_, body), _) => Json.parse[T](body.asString).map((url, _))
    case _ => None
  }
}

However, as soon as I add a new model (and associated JsonReader) the code no longer compiles with this error:

ambiguous implicit values:
[error]  both value personFormat in object Json of type => spray.json.RootJsonFormat[com.rsslldnphy.foam.models.Person]
[error]  and value animalFormat in object Json of type => spray.json.RootJsonFormat[com.rsslldnphy.foam.models.Animal]
[error]  match expected type spray.json.JsonReader[T]
[error]     case Post (Routes.person.post, p: Person) => sender ! Ok(Actions.person.post(p))

The fact that the generic types of the JsonReaders are different appears to be lost. Is this type erasure? Is there a way around it to get what I want?

Here's the full, compiling code of the project so far with a comment in ExampleService that explains what makes it break: github.com/rsslldnphy/foam . Your help is appreciated, thanks.

Or if what I want isn't currently possible, can anyone suggest an alternative approach?

You need to give the compiler explicit instructions to make this work. As you can see below, there is no way the compiler can infere what T is supposed to be. The compiler would need to be able to dynamically look at the Json from the request and from that imply a type (which we can only dream of it doing ;))

def unapply[T:JsonReader](req: HttpRequest): Option[(String, T)] = (...) Json.parse[T] (...)

That means to make this work you have to explicitly annotate the post as shown below:

import MyJsonProtocol._

...

def receive = {
  case Post[Person] (Routes.person.post, p: Person) => sender ! Ok(Actions.person.post(p))
  case Get  (Routes.foo.forId(x))           => sender ! Ok(x)
  case _                                    => sender ! Ok("No handler")
}

and change the definition to this

case class Post[T: JsonReader] extends Request {
  def unapply(req: HttpRequest): Option[(String, T)] = req match {
    case HttpRequest(POST, url, _, HttpBody(_, body), _) => Json.parse[T](body.asString).map((url, _))
    case _ => None
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM