0

I have a converter function that creates a case class object from JSON, which is defined as follows:

object JSONConverter {
  import language.experimental.macros

  def modelFromJson[T <: Model](json: JsValue):T = macro modelFromJson_impl[T]
}

I'm interested in creating the same object from a String that contains JSON, too. So I have overloaded the original method by changing the above snippet as follows:

object JSONConverter {
  import language.experimental.macros

  // Using Play Framework library to parse a String to JSON
  def modelFromJson[T <: Model](json: JsValue):T = macro modelFromJson_impl[T]

  def modelFromJson[T <: Model](jsonString: String):T = {
    val json: JsValue = Json.parse(jsonString)
    modelFromJson[T](json)
  }
}

But I get this error message:

[error] ... not found: value T
[error]     modelFromJson[T](json)
[error]                     ^

So for the following configuration

case class User(name: String, age: Int, posts:String, score: Double, lastLogin: DateTime) extends Model

// First case
JSONConverter.modelFromJson[User](someJsValueObject)

// Second case
JSONConverter.modelFromJson[User](someJsonString)

The macro tries to return the following expressions:

User.apply(json.$bslash("name").as[String], json.$bslash("age").as[Int], json.$bslash("posts").as[String], json.$bslash("score").as[Double], new DateTime(json.$bslash("lastLogin").as[Long]))

T.apply()

The first one is correct, while the second one tries to access T instead.

The macro is basically implemented as follows:

def modelFromJson_impl[T: c.WeakTypeTag](c: Context)(json: c.Expr[JsValue]): c.Expr[T] = {
  import c.universe._
  val tpe = weakTypeOf[T]

  // Case class constructor
  val generation = Select(Ident(newTermName(tpe.typeSymbol.name.decoded)), newTermName("apply")) 

  // Generate stuff like json.\("fieldName").as[String]
  val fields = tpe.declarations collect {
      ... 
  }

  val s = Apply(generation, fields.toList)

  // Display results above for debugging
  println(show(s))

  c.Expr[T](s)
}

Is it possible to achieve what I want without making another macro?

Thanks very much in advance!

Emre
  • 1,023
  • 2
  • 9
  • 24
  • 1
    I don't think that's possible. Unfortunately one can't abstract over macros at runtime, at least, not in Scala. – Eugene Burmako Jun 25 '13 at 16:26
  • Are there any plans to support this in future? – Emre Jun 27 '13 at 08:02
  • Not sure how exactly to implement that. What would you suggest? – Eugene Burmako Jun 27 '13 at 09:11
  • I have almost no idea how a compiler works (yeah, I know), but I think it might be possible to understand if something is generic, no? If we know that a given type is generic, I think we have enough information at compile time to find out what T is, since we can find all the functions that call the overloaded method. So the macro expansion searches for a further step (or further steps) until all of the generics are resolved, then generates the functions according to that. – Emre Jun 27 '13 at 17:45
  • I have no idea how difficult or feasible doing that is, obviously. But I think this functionality is somewhat fundamental because of two reasons. Macros are probably going to be used extensively in basic building blocks (e.g. serialization, like above) where other devs will build things upon. They take time to learn and write, so people will probably try to adapt their needs to use existing macros in a project, just like the way I did above. – Emre Jun 27 '13 at 17:54
  • The second reason is that it is quite _natural_ to try to use it like this. Scala is very good at being natural. I'm a beginner, but I found Scala to be quite intuitive. It generally works the way I think it should work (which is extremely great), so people will somewhat expect their intuition to work here too when macros become popular. – Emre Jun 27 '13 at 17:58
  • Would be glad to help if there's anything I can do. – Emre Jun 27 '13 at 18:02
  • I see, so if I understand correctly, you propose to automatically convert `def modelFromJson[T <: Model](jsonString: String):T` to a macro? – Eugene Burmako Jun 30 '13 at 08:22
  • @EugeneBurmako Hmm, that might be one way to look at it I think. But escalating the expansion to another function might complicate things and make macros spread the "infection" by constantly converting others into macros as things build upon them. I'm not sure that would be desirable. What I tried to say is, while a macro is being expanded, it should check if all types are resolved. – Emre Jul 01 '13 at 17:18
  • For instance, we know that a macro is called. While expanding, the compiler asks: "Is `T` a valid type, or a generic?". If it is a valid type then it does a standard macro expansion. If not, then it looks for the places at which the function that calls itself is called, then obtains the types from there. This could go on for any level of depth. – Emre Jul 01 '13 at 17:25
  • As a concrete example, when `def modelFromJson[T <: Model](json: JsValue):T` is called from `def modelFromJson[T <: Model](jsonString: String):T`, the compiler tries to expand the macro, but finds out that `T` is not a valid type. It goes one level further and checks where `def modelFromJson[T <: Model](jsonString: String):T` is called, which is `JSONConverter.modelFromJson[User](someJsonString)`. `T` is replaced by `User` and then the original macro is expanded as such, as all types are resolved. – Emre Jul 01 '13 at 17:26
  • There could be multiple overloadings with multiple concrete types, so the compiler will actually need to traverse a tree of references I think, doing an expansion for each leaf. – Emre Jul 01 '13 at 17:28
  • 1) What to do with the cases when a macro makes sense to be expanded even if a type parameter is involved (e.g. a macro that transforms `foreach` into an equivalent `while` doesn't care about the type, it just needs to expand)? 2) What if the call graph cannot be reliably analyzable, e.g. when there are ifs involved? – Eugene Burmako Jul 03 '13 at 06:14
  • Sorry for the very late reply. I'm not sure if I understand your first question. What happens in the current case, like when you call your example macro on a List[Int]? Won't the compiler generate code for actual types in place of generics? For your second question, we could take the pessimistic approach and assume that if a macro is called somewhere (e.g. `if (cond) callOverloadedMacro[MyType](foo)`) then it will be expanded for that type. Perhaps a more concrete case will help me understand the problem better? – Emre Oct 23 '13 at 14:15

1 Answers1

0

I worked around the problem by moving the stuff to compile time. I defined another macro as

def modelFromJsonString[T <: Model](jsonString: String):T = macro convertFromJsonString_impl[T]

def convertFromJsonString_impl[T: c.WeakTypeTag](c: Context)(jsonString: c.Expr[String]): c.Expr[T] = {
  import c.universe._
  val conversion = c.Expr[JsValue](reify(Json.parse(jsonString.splice)).tree)

  convertFromJson_impl[T](c)(conversion)
}
Emre
  • 1,023
  • 2
  • 9
  • 24