Matt Roberts
Matt Roberts

Reputation: 1107

How to check if some T is a case class at compile time in Scala?

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.

Upvotes: 5

Views: 1258

Answers (2)

Sam Upra
Sam Upra

Reputation: 737

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

Upvotes: 1

Matt Roberts
Matt Roberts

Reputation: 1107

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.

Upvotes: 3

Related Questions