2

I have defined the following macros to get file, line and object/class from current location: http://pastebin.com/UsNLemnK

Using SBT, I have defined two projects, in order to compile the macros first, then the actual project using these macros.

The purpose of these macros are to be be used in a log method:

def log( msg: Any, srcFile: String = "", srcLine: String = "", srcClass:String = "")

I am then using this log method as follows:

log(msg, s"$F_",s"$L_",s"$C_")

where F_, L_ and C_ are defined in the macro.

Now, I would like to create a shortcut to avoid this boilerplate and just call:

log(msg)

which should automatically be replaced by

log(msg, s"$F_",s"$L_",s"$C_")

I could define a macro to do this:

def log_(msg: String) : Unit = macro logImpl
def logImpl( c: Context )(msg: c.Expr[String]): c.Expr[Unit] = {
  import c.universe._
  reify( log(msg.splice, srcFile=s"$F_", srcLine=s"$L_", srcClass=s"$C_") )
}

but again, this macro needs to be compiled before the project, where the log function itself is defined... So I don't see how to solve the compilation dependencies cycle...

Any suggestion about how to do this? Thanks

borck
  • 928
  • 10
  • 19
  • What's stopping you from moving the `log` function to the macro project? – mikołak Feb 25 '14 at 16:53
  • It does not feel "clean" as the log function is quite involved... I would have to move many parts from my core project to the macro project. I was hoping there would be a better way – borck Feb 25 '14 at 17:03

1 Answers1

2

Barring the use of macro annotations (which would necessarily and significantly alter your API's syntax), the problem you have to face is that you need the type-checked identifier of your log function.

Since you can't import the entire log implementation, a solution would be to:

  • wrap the method into a trait,
  • define this trait in the "macro" project,
  • add an implicit parameter to the log_ method,
  • in your "main" project, create an implementation of this trait, and instantiate this implementation in an implicit val visible everywhere you'd like to use the log_ macro (in the package object for example).

Of course, you could also use a simple FunctionN here and avoid the trait definition and implementation, but this way you'll avoid potential conflicts with other same-typed implicits.

In general, your code would resemble the following:

//"macro" project
trait EncapsulatingTrait {
  def yourMethod(...)
}

object Macros {
  def myMacro(...)(implicit param: EncapsulatingTrait) = macro myMacroImpl
  def myMacroImpl( c: Context )(...)
                          (param: c.Expr[EncapsulatingTrait]): c.Expr[...] = {
    import c.universe._
    reify(param.splice.yourMethod(...))
  }
}

//--------------------------
//"main" project
class Impl extends EncapsulatingTrait {
  def yourMethod(...)
}

...

implicit val defaultParam = new Impl

import Macros.myMacro

myMacro(...)

In your specific case, here's how an implementation could look like:

//"macro" project
package yourpackage

import java.io.File
import language.experimental.macros
import scala.reflect.macros.Context

trait LogFunction {
  def log( msg: Any, srcFile: String = "", srcLine: Int = -1, srcClass:String = "")
}


object Macros {
      // get current line in source code
      def L_ : Int = macro lineImpl
      def lineImpl( c: Context ): c.Expr[Int] = {
        import c.universe._
        val line = Literal( Constant( c.enclosingPosition.line ) )
        c.Expr[Int]( line )
      }

      // get current file from source code (relative path)
      def F_ : String = macro fileImpl
      def fileImpl( c: Context ): c.Expr[String] = {
        import c.universe._
        val absolute = c.enclosingPosition.source.file.file.toURI
        val base = new File( "." ).toURI
        val path = Literal( Constant( c.enclosingPosition.source.file.file.getName() ) )
        c.Expr[String]( path )
      }

      // get current class/object (a bit sketchy)
      def C_ : String = macro classImpl
      def classImpl( c: Context ): c.Expr[String] = {
        import c.universe._

        val class_ = Literal( Constant( c.enclosingClass.toString.split(" ")( 1 ) ) )  
        c.Expr[String]( class_ )
      }


     def log_(msg: String)(implicit logFunc: LogFunction) : Unit = macro logImpl
     def logImpl( c: Context )(msg: c.Expr[String])(logFunc: c.Expr[LogFunction]): c.Expr[Unit] = {
      import c.universe._
      reify( logFunc.splice.log(msg.splice, srcFile=fileImpl(c).splice, srcLine=lineImpl(c).splice, srcClass=classImpl(c).splice) )
     }
}


//--------------------------
//"main" project
import yourpackage.LogFunction

class LogImpl extends LogFunction {
  def log( msg: Any, srcFile: String = "", srcLine: Int = -1, srcClass:String = "") {
    println(List(msg,srcFile,srcLine,srcClass).mkString("|"))
  }
}

object testLog {

  def main(args: Array[String]): Unit = {

    implicit val defaultLog = new LogImpl

    import yourpackage.Macros.log_

    log_("blah")

  }

}

(note that I had to correct the signature of log_ and tweak the macro call a bit)

mikołak
  • 9,605
  • 1
  • 48
  • 70
  • Works perfectly thanks. One more question, if I declare the implicit val in the highest-level package object, how can I make it accessible to all children packages ? – borck Feb 26 '14 at 09:37
  • 1
    Nevermind, I found the answer to that here : http://stackoverflow.com/questions/2830248/what-are-nested-unnested-packages-in-scala-2-8 – borck Feb 26 '14 at 09:41