Chris
Chris

Reputation: 1094

How to enforce non-generic type at compile time

consider a generic function:

def genericFn[T](fn: T => Boolean): Unit = {
  // do something involves T
}

is it possibile to restrict T (at compile time) to be a simple type, not a type like List[Int]?


the underling problem I want to solve is something like this:

var actorReceive: Receive = PartialFunction.empty
def addCase[T](handler: T => Boolean): Unit = {
    actorReceive = actorReceive orElse ({
        case msg: T => // call handle at some point, plus some other logic
            handler(msg)
    })
}

the addCase function would result in type erasure warning, which could be solved by requiring ClassTag like: def addCase[T: ClassTag](..., but ClassTag still can't guard against calls like:

addCase[List[Int]](_ => {println("Int"); true})
addCase[List[String]](_ => {println("String"); false})

actorReceive(List("str"))    // will print "Int"

the above code will print "Int" while not issuing any warning or error at all, is there any way out?

Upvotes: 5

Views: 262

Answers (2)

Michael Zajac
Michael Zajac

Reputation: 55569

There is no way to enforce this in the type system as-is, without reflection.

The nicest way to do this would be to have a type-class such as NonEraseable[A], that provides evidence that a type has no type parameters that would be erased at runtime. An implicit NonEraseable[A] in scope should mean that A has no type parameters. Seeing as these would be tedious to manually create, an implicit macro can do the job:

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

trait NonEraseable[A]

object NonEraseable {

    implicit def ev[A]: NonEraseable[A] = macro evImpl[A]

    def evImpl[A](c: Context)(implicit tt: c.WeakTypeTag[A]): c.Expr[NonEraseable[A]] = {
        import c.universe._
        val tpe = weakTypeOf[A]
        if(tpe.dealias.typeArgs.isEmpty)
            c.Expr[NonEraseable[A]](q"new NonEraseable[$tpe] {}")
        else
            c.abort(c.enclosingPosition, s"$tpe contains parameters that will be erased at runtime.")
    }

}

Use case:

def onlySimple[A : NonEraseable](value: A): Unit = println(value)

scala> onlySimple(1)
1

scala> onlySimple(List(1, 2, 3))
<console>:13: error: List[Int] contains parameters that will be erased at runtime.
       onlySimple(List(1, 2, 3))
                 ^

Using this, you can enforce at compile time that a type parameter A with a context bound NonEraseable is the kind of type you want. (Assuming you don't cheat and manually create instance of the type class)

Upvotes: 5

Alexey Romanov
Alexey Romanov

Reputation: 170899

You can at least get it to fail at run-time as follows:

def addCase[T: ClassTag](handler: T => Boolean): Unit =
  if (classTag[T].runtimeClass.getTypeParameters.nonEmpty) {
    // throw an exception
  } else {
    // the main code
  }

Compile-time failure can be achieved using a macro instead of a function (approximate, untested):

def addCase[T](handler: T => Boolean): Unit = macro addCaseImpl

def addCaseImpl[T: c.WeakTypeTag](c: Context)(handler: c.Expr[T => Boolean]): c.Expr[Unit] =
  if (c.weakTypeOf[T].typeParams.nonEmpty) {
    c.abort(c.enclosingPosition, "Generic types not allowed in addCase")
  } else {
    // generate code for main line
  }

Upvotes: 3

Related Questions