0

I'm having some problems with a macro I've written to help me log metrics represented as case class instances to to InfluxDB. I presume I'm having a type erasure problem and that the tyep parameter T is getting lost, but I'm not entirely sure what's going on. (This is also my first exposure to Scala macros.)

import scala.language.experimental.macros
import play.api.libs.json.{JsNumber, JsString, JsObject, JsArray}

abstract class Metric[T] {
    def series: String

    def jsFields: JsArray = macro MetricsMacros.jsFields[T]
    def jsValues: JsArray = macro MetricsMacros.jsValues[T]
}

object Metrics {
    case class LoggedMetric(timestamp: Long, series: String, fields: JsArray, values: JsArray)
    case object Kick

    def log[T](metric: Metric[T]): Unit = {
        println(LoggedMetric(
            System.currentTimeMillis,
            metric.series,
            metric.jsFields,
            metric.jsValues
        ))
    }
}

And here's an example metric case class:

case class SessionCountMetric(a: Int, b: String) extends Metric[SessionCountMetric] {
    val series = "sessioncount"
}

Here's what happens when I try to log it:

scala> val m = SessionCountMetric(1, "a")
m: com.confabulous.deva.SessionCountMetric = SessionCountMetric(1,a)

scala> Metrics.log(m)
LoggedMetric(1411450638296,sessioncount,[],[])

Even though the macro itself seems to work fine:

scala> m.jsFields
res1: play.api.libs.json.JsArray = ["a","b"]

scala> m.jsValues
res2: play.api.libs.json.JsArray = [1,"a"]

Here's the actual macro itself:

import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context

object MetricsMacros {
    private def fieldNames[T: c.WeakTypeTag](c: Context)= {
        val tpe = c.weakTypeOf[T]
        tpe.decls.collect {
            case field if field.isMethod && field.asMethod.isCaseAccessor => field.asTerm.name
        }
    }

    def jsFields[T: c.WeakTypeTag](c: Context) = {
        import c.universe._
        val names = fieldNames[T](c)
        Apply(
            q"play.api.libs.json.Json.arr",
            names.map(name => Literal(Constant(name.toString))).toList
        )
    }

    def jsValues[T: c.WeakTypeTag](c: Context) = {
        import c.universe._
        val names = fieldNames[T](c)
        Apply(
            q"play.api.libs.json.Json.arr",
            names.map(name => q"${c.prefix.tree}.$name").toList
        )
    }
}

Update

I tried Eugene's second suggestion like this:

abstract class Metric[T] {
    def series: String
}

trait MetricSerializer[T] {
    def fields: Seq[String]
    def values(metric: T): Seq[Any]
}

object MetricSerializer {
    implicit def materializeSerializer[T]: MetricSerializer[T] = macro MetricsMacros.materializeSerializer[T]
}

object Metrics {
    def log[T: MetricSerializer](metric: T): Unit = {
        val serializer = implicitly[MetricSerializer[T]]
        println(serializer.fields)
        println(serializer.values(metric))
    }
}

with the macro now looking like this:

object MetricsMacros {
    def materializeSerializer[T: c.WeakTypeTag](c: Context) = {
        import c.universe._

        val tpe = c.weakTypeOf[T]
        val names = tpe.decls.collect {
            case field if field.isMethod && field.asMethod.isCaseAccessor => field.asTerm.name
        }

        val fields = Apply(
            q"Seq",
            names.map(name => Literal(Constant(name.toString))).toList
        )

        val values = Apply(
            q"Seq",
            names.map(name => q"metric.$name").toList
        )

        q"""
            new MetricSerializer[$tpe] {
                def fields = $fields
                def values(metric: Metric[$tpe]) = $values
            }
        """
    }
}

However, when I call Metrics.log -- specifically when it calls implicitly[MetricSerializer[T]] I get the following error:

error: value a is not a member of com.confabulous.deva.Metric[com.confabulous.deva.SessionCountMetric]

Why is it trying to use Metric[com.confabulous.deva.SessionCountMetric] instead of SessionCountMetric?

Conclusion

Fixed it.

def values(metric: Metric[$tpe]) = $values

should have been

def values(metric: $tpe) = $values
Isvara
  • 3,403
  • 1
  • 28
  • 42

1 Answers1

2

You're in a situation that's very close to one described in a recent question: scala macros: defer type inference.

As things stand right now, you'll have to turn log into a macro. An alternative would also to turn Metric.jsFields and Metric.jsValues into JsFieldable and JsValuable type classes materialized by implicit macros at callsites of log (http://docs.scala-lang.org/overviews/macros/implicits.html).

Community
  • 1
  • 1
Eugene Burmako
  • 13,028
  • 1
  • 46
  • 59
  • I tried your alternative, as it seemed like the cleaner way overall, but I then hit a different problem. Please see the update in my edit. – Isvara Sep 24 '14 at 05:33
  • Actually, it was a quick fix -- see conclusion. Thanks! – Isvara Sep 24 '14 at 05:47