5

I have the following macro:

package macros

import scala.reflect.macros.blackbox.Context

object CompileTimeAssertions {
  def mustBeCaseClass[T]: Unit =
    macro CompileTimeAssertionsImpl.mustBeCaseClass[T]
}

object CompileTimeAssertionsImpl {
  def mustBeCaseClass[T: c.WeakTypeTag](c: Context): c.Expr[Unit] = {
    import c.universe._
    val symbol = c.weakTypeTag[T].tpe.typeSymbol
    if (!symbol.isClass || !symbol.asClass.isCaseClass) {
      c.error(c.enclosingPosition, s"${symbol.fullName} must be a case class")
    }
    reify(Unit)
  }
}

It works when generics aren't involved, but fails when they are:

import macros.CompileTimeAssertions._
import org.scalatest.{Matchers, WordSpec}

case class ACaseClass(foo: String, bar: String)

class NotACaseClass(baz: String)

class MacroSpec extends WordSpec with Matchers {
  "the mustBeCaseClass macro" should {
    "compile when passed a case class" in {
      mustBeCaseClass[ACaseClass]
    }

    "not compile when passed a vanilla class" in {
//      mustBeCaseClass[NotACaseClass] // fails to compile as expected.
    }

    "compile when working with generics" in {
//      class CaseClassContainer[T] { mustBeCaseClass[T] } // fails to compile.
//      new CaseClassContainer[ACaseClass]
    }
  }
}

The compiler error is mine:

MacroSpec.CaseClassContainer.T must be a case class

I'd like to find out what T is when the CaseClassContainer is instantiated. Is that even possible? If it is can you provide an example?

Thanks in advance.

Matt Roberts
  • 1,107
  • 10
  • 15
  • 1
    Unfortunately, what you're trying to do is impossible to achieve in this fashion. Def macros expand immediately at the place when you write them, so at the point of the expansion all that you're going to see is T. – Eugene Burmako May 14 '15 at 10:12
  • 2
    However, if you can change your approach to incorporate typeclasses, you might be able to get somewhere. Namely: you could have a `CaseClass[T]` type class and then you'll be able to say `class CaseClassContainer[T: CaseClass]`. Check out https://github.com/scalamacros/macrology201/tree/part2 to see how one could approach writing such macros. – Eugene Burmako May 14 '15 at 10:14
  • Thanks Eugene! Please feel free to copy and paste these into an answer and I'll mark it as correct. No is an answer and the maybe is a whole lot better than where I was 13 minutes ago. – Matt Roberts May 14 '15 at 10:26
  • 1
    Note that Twitter's bijection library provides an `IsCaseClass` type class where instances are generated by [a macro](https://github.com/twitter/bijection/blob/develop/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/IsCaseClassImpl.scala). – Travis Brown May 14 '15 at 12:42
  • I think Travis's comment deserves to be an answer instead :) – Eugene Burmako May 14 '15 at 14:17
  • Thanks Travis. As long as you're happy with that Eugene. I mean you're both proposing the same (correct) thing. If I get a chance I'll try and code up and post the solution for others. Thanks again. – Matt Roberts May 14 '15 at 16:45

2 Answers2

3

Thanks to Eugene and Travis' advice I was able to solve this problem with type classes. Here's the solution:

package macros

import scala.reflect.macros.blackbox.Context

trait IsCaseClass[T]

object IsCaseClass {
  implicit def isCaseClass[T]: IsCaseClass[T] =
    macro IsCaseClassImpl.isCaseClass[T]
}

object IsCaseClassImpl {
  def isCaseClass[T]
      (c: Context)
      (implicit T: c.WeakTypeTag[T]): c.Expr[IsCaseClass[T]] = {
    import c.universe._
    val symbol = c.weakTypeTag[T].tpe.typeSymbol
    if (!symbol.isClass || !symbol.asClass.isCaseClass) {
      c.abort(c.enclosingPosition, s"${symbol.fullName} must be a case class")
    } else {
      c.Expr[IsCaseClass[T]](q"_root_.macros.IsCaseClassImpl[$T]()")
    }
  }
}

case class IsCaseClassImpl[T]() extends IsCaseClass[T]

And here is the usage:

import macros.IsCaseClass
import org.scalatest.{Matchers, WordSpec}

case class ACaseClass(foo: String, bar: String)

class NotACaseClass(baz: String)

class CaseClassContainer[T: IsCaseClass]

class MacroSpec extends WordSpec with Matchers {
  "the code" should {
    "compile" in {
      new CaseClassContainer[ACaseClass]
    }

    "not compile" in {
//      new CaseClassContainer[NotACaseClass]
    }
  }
}

Worth noting the use of abort instead of error. Abort returns Nothing whereas error returns Unit. The latter was fine when the macro wasn't returning anything.

Matt Roberts
  • 1,107
  • 10
  • 15
1

In scala 2.11 and above, it's much simplier now. I've created a small project that does it : https://github.com/samupra/CaseClassChecker

Sam Upra
  • 737
  • 5
  • 12