Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 889
Chris Avatar asked Nov 04 '15 17:11

Chris


2 Answers

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)

like image 155
Michael Zajac Avatar answered Oct 27 '22 00:10

Michael Zajac


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
  }
like image 45
Alexey Romanov Avatar answered Oct 27 '22 00:10

Alexey Romanov