4

I am trying to abstract out the json parsing logic that gets triggered for a specific type.

I started out creating a Parser trait as follows:

trait Parser {
  def parse[T](payload : String) : Try[T]
}

I have an implementation of this trait called JsonParser which is:

class JsonParser extends Parser {

  override def parse[T](payload: String): Try[T] = parseInternal(payload)

  private def parseInternal[T:JsonParserLike](payload:String):Try[T] = {
    implicitly[JsonParserLike[T]].parse(payload)
  }
} 

The JsonParserLike is defined as follows:

trait JsonParserLike[T] {
  def parse(payload: String): Try[T]
}

object JsonParserLike {
  implicit val type1Parser:JsonParserLike[Type1] = new JsonParserLike[Type1] 
  {
    //json parsing logic for Type1
  }

  implicit val type2Parser:JsonParserLike[Type2] = new JsonParserLike[Type2]
  {
     //json parsing logic for Type2
  }
}

When I try compiling the above, the compilation fails with:

ambiguous implicit values:
[error]  both value type1Parse in object JsonParserLike of type => parser.jsonutil.JsonParserLike[parser.models.Type1]
[error]  and value type2Parser in object JsonParserLike of type => parser.jsonutil.JsonParserLike[parser.models.Type2]
[error]  match expected type parser.jsonutil.JsonParserLike[T]
[error]   override def parse[T](payload: String): Try[T] = parseInternal(payload)

Not sure why the implicit resolution is failing here. Is it because the parse method in the Parser trait doesn't have an argument of type parameter T?

I tried another approach as follows:

trait Parser {
  def parse[T](payload : String) : Try[T]
}

class JsonParser extends Parser {

  override def parse[T](payload: String): Try[T] = {
    import workflow.parser.JsonParserLike._
    parseInternal[T](payload)
  }

  private def parseInternal[U](payload:String)(implicit c:JsonParserLike[U]):Try[U] = {
     c.parse(payload)
  }
}

The above gives me the following error:

could not find implicit value for parameter c: parser.JsonParserLike[T]
[error]     parseInternal[T](payload)
[error]  

               ^

Edit: Adding the session from the REPL

scala> case class Type1(name: String)
defined class Type1

scala> case class Type2(name:String)
defined class Type2

scala> :paste
// Entering paste mode (ctrl-D to finish)

import scala.util.{Failure, Success, Try}

trait JsonParserLike[+T] {
  def parse(payload: String): Try[T]
}

object JsonParserLike {

  implicit val type1Parser:JsonParserLike[Type1] = new JsonParserLike[Type1] {

    override def parse(payload: String): Try[Type1] = Success(Type1("type1"))
  }

  implicit val type2Parser:JsonParserLike[Type2] = new JsonParserLike[Type2] {

    override def parse(payload: String): Try[Type2] = Success(Type2("type2"))
  }
}

// Exiting paste mode, now interpreting.

import scala.util.{Failure, Success, Try}
defined trait JsonParserLike
defined object JsonParserLike

scala> :paste
// Entering paste mode (ctrl-D to finish)

trait Parser {
  def parse[T](payload : String) : Try[T]
}

class JsonParser extends Parser {

  override def parse[T](payload: String): Try[T] = parseInternal(payload)

  private def parseInternal[T:JsonParserLike](payload:String):Try[T] = {
    implicitly[JsonParserLike[T]].parse(payload)
  }
}

// Exiting paste mode, now interpreting.

<pastie>:24: error: ambiguous implicit values:
 both value type1Parser in object JsonParserLike of type => JsonParserLike[Type1]
 and value type2Parser in object JsonParserLike of type => JsonParserLike[Type2]
 match expected type JsonParserLike[T]
  override def parse[T](payload: String): Try[T] = parseInternal(payload)
sc_ray
  • 7,803
  • 11
  • 63
  • 100
  • That's a fairly strange error message, but it's unclear what you tried there in the first place. There is no reason why `def parse[T](payload: String): Try[T] = parseInternal(payload)` should compile: where is it supposed to get the `JsonParserLike`? Every type parameter that "comes from the outside" (`T`, in this case), always has to bring all the required type classes with it, but there is no `JsonParserLike` in the signature of `parse`. Where is it supposed to come from? It must be inserted by the compiler at the call site, it cannot materialize out of thin air. – Andrey Tyukin Jul 15 '18 at 21:25
  • @AndreyTyukin - I imported the the JsonParserLike._ before the class definition. I don't see why the method won't compile. – sc_ray Jul 15 '18 at 21:31
  • The imports are irrelevant in this case. The definition `override def parse[T](payload: String): Try[T] = parseInternal(payload)` cannot work in principle, because `parseInternal` relies on a typeclass for `T`, but the enclosing method `parse` does not take any typeclasses at all. There is no pathway that can bring the typeclass instance from the call site to `parseInternal`. – Andrey Tyukin Jul 15 '18 at 21:35
  • The only way to make it work would be to analyze the `String` argument at runtime, and then somehow summon an appropriate instance of `JsonParserLike` inside of `parse`, and then hope that it actually parses the things of the right type `T` (one would probably have to persuade the compiler with an `asInstanceOf[T]` with such an approach). – Andrey Tyukin Jul 15 '18 at 21:38
  • @AndreyTyukin - I added the REPL session to the OP. – sc_ray Jul 15 '18 at 21:41
  • Don't see any significant changes. The definition `override def parse[T](payload: String): Try[T] = parseInternal(payload)` is still there. The `parseInternal` still **requires** a `JsonParserLike`. The `parse` still **doesn't provide** a `JsonParserLike`. – Andrey Tyukin Jul 15 '18 at 21:45
  • When you say requires, you mean it needs to be passed in as a parameter? – sc_ray Jul 15 '18 at 21:51
  • I was under the impression that private def parseInternal[T:JsonParserLike](payload:String) takes the JsonParserLike from the implicit context. – sc_ray Jul 15 '18 at 21:51
  • @sc_ray, Could you describe why do you want `Parser` with such signature in the first place? This approach seems to me quiet un-Scala-like. It essentially breaks any type safety because it says that if an object implements `Parser` trait, it can parse `String` in a value of any type `T` (which is obviously impossible). So what are you really trying to achieve by introducing that trait? Why don't you start with `implicit` parameters in the first place like your `JsonParserLike` and like many Scala JSON libraries do? – SergGr Jul 15 '18 at 23:37
  • @SergGr - It is very interesting that you think my approach in un-scala-like. The original intent was to have a `parse` that takes a `T` and returns an `R`. Not sure how this breaks type-safety since the implementor of `Parser` has the requisite capability to perform the transformation on the `String`. If you could elaborate on your approach in an answer, that might be super helpful. Also, the original intent of the question was to get some clarity on why the implicit resolution was not working. – sc_ray Jul 16 '18 at 03:57
  • @sc_ray, if your `Parser` was a generic over `T`, then it would be what you say. But your `Parser` is a non-generic and only its `parse[T]` is generic. And since `T` is not restricted in any way, if I have `val p:Parser`, then I can write something like `p.parse[Thread](str)` or `p.parse[java.sql.Connection](str)` and this will compile. This is what I call breaking type safety. Introducing `implicit` parameters on the other hand is one of the ways Scala provides for restricting `T` (you essentially say that you can parse to any type that conforms to the corresponding typeclass). – SergGr Jul 16 '18 at 12:26
  • @SergGr That was a good insight. I can definitely restrict the type parameters. When you say introducing implicit parameters, are you suggesting have a method in the Parser trait that accepts a curried implicit Jason type decoder as a parameter? – sc_ray Jul 16 '18 at 15:32
  • @sc_ray, it is really hard to provide you help because you avoid showing the context i.e. you show implementation but not how this stuff is supposed to be used. Yes, you can add an `implicit` parameter to `Parser.parse` but then there is a question why do you need `Parser` in the first place? Because all the real work will be done by the implicit parameter anyway, There are a few good Scala JSON libraries like [circe](https://github.com/circe/circe), [spray-json](https://github.com/spray/spray-json) or [argonaut](http://argonaut.io/). Take a look at their design decisions. – SergGr Jul 16 '18 at 16:50
  • @SergGr I don’t think there’s anything magical about how a ‘trait’ is supposed to be used. The fact that I am using circe/spray-json/argonaut is an implementation detail and is supposed to be hidden by the typeclass. My question goes beyond which json parser to use and more about why the implicit resolution is failing. – sc_ray Jul 16 '18 at 17:15

2 Answers2

4

As I've already tried to explain in the comments, the problem is that the method

override def parse[T](payload: String): Try[T] = parseInternal(payload)

does not accept any JsonParserLike[T] instances. Therefore, the compiler has no way to insert the right instance of JsonParserLike[T] at the call site (where the type T is known).

To make it work, one would have to add some kind of token that uniquely identifies type T to the argument list of parse. One crude way would be to add a JsonParserLike[T] itself:

import util.Try

trait Parser {
  def parse[T: JsonParserLike](payload : String) : Try[T]
}

class JsonParser extends Parser {

  override def parse[T: JsonParserLike](payload: String): Try[T] = 
    parseInternal(payload)

  private def parseInternal[T:JsonParserLike](payload:String):Try[T] = {
    implicitly[JsonParserLike[T]].parse(payload)
  }
} 

trait JsonParserLike[T] {
  def parse(payload: String): Try[T]
}

object JsonParserLike {
  implicit val type1Parser: JsonParserLike[String] = ???
  implicit val type2Parser: JsonParserLike[Int] = ???
}

Now it compiles, because the JsonParserLike[T] required by parseInternal is inserted automatically as an implicit parameter to parse.

This might be not exactly what you want, because it creates a hard dependency between Parser interface and the JsonParserLike typeclass. You might want to get some inspiration from something like shapeless.Typeable to get rid of the JsonParserLike in the Parser interface, or just rely on circe right away.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • You are right. I would like to avoid the dependency between Parser and JsonParserLike. I will take a look at shapeless. Thanks – sc_ray Jul 15 '18 at 21:58
  • @sc_ray I'm not sure that the mentioned Shapeless feature is the right tool for the task, it was rather just a vague feeling that there is some technique that could be useful in such cases... I think the currently more popular solution would be to use Circe. – Andrey Tyukin Jul 15 '18 at 22:02
  • I looked at circe. It seems like I will still need to have specific decoders for the type 'T', even while using circe, a Typeclass will provide quite a bit of flexibility to dispatch the decode logic for a specific type. – sc_ray Jul 15 '18 at 22:08
1

It seems like there is an extra complexity from a mix of different types of polymorphism in both examples. Here is a minimal example of just a type class:

// type class itself
trait JsonParser[T] {
  def parse(payload: String): Try[T]
}

// type class instances
object JsonParserInstances {
  implicit val type1Parser: JsonParser[Type1] = new JsonParser[Type1] {
    def parse(payload: String): Try[Type1] = ???
  }

  implicit val type2Parser: JsonParser[Type2] = new JsonParser[Type2] {
    def parse(payload: String): Try[Type2] = ???
  }
}

// type class interface
object JsonInterface {
  def parse[T](payload: String)(implicit T: JsonParser[T]): Try[T] = {
    T.parse(payload)
  }
}

def main(args: Array[String]): Unit = {
  import JsonParserInstances._
  JsonInterface.parse[Type1]("3")
  JsonInterface.parse[Type2]("3")
}

More info:

MichaelDay
  • 168
  • 1
  • 1
  • 8