Search code examples
scalascala-macrosdependent-typerefined

Scala: how to force converting a statement to literal?


I'm experimenting the refined type feature of scala provided in one of its library:

https://github.com/fthomas/refined

The following code represents a simple case:

  import eu.timepit.refined.auto._
  import shapeless.{Witness => W}

    type Vec5 = List[Int] Refined Size[Equal[W.`5`.T]]

    val v1: Vec5 = List(1, 2, 3, 4, 5)

    val v2: Vec5 = List(1 to 5: _*)

When attempting to compile it I got the following error:


[Error] /home/peng/git/scalaspike/common/src/test/scala/com/tribbloids/spike/refined_spike/Example.scala:32: compile-time refinement only works with literals
[Error] /home/peng/git/scalaspike/common/src/test/scala/com/tribbloids/spike/refined_spike/Example.scala:34: compile-time refinement only works with literals
[Error] /home/peng/git/scalaspike/common/src/test/scala/com/tribbloids/spike/singleton_ops_spike/Example.scala:32: Cannot prove requirement Require[...]
three errors found

It should be noted that both v1 & v2 can be easily evaluated at compile time and inlined, however scala compiler seems to refuse to do that, and for List type there seems to have no way to suggest this.

So how could this feature be useful?


Solution

  • The thing is that eu.timepit.refined macros work for literals, BigDecimal, BigInt

    https://github.com/fthomas/refined/blob/master/modules/core/shared/src/main/scala/eu/timepit/refined/macros/RefineMacro.scala#L14-L23

    def impl[F[_, _], T: c.WeakTypeTag, P: c.WeakTypeTag](t: c.Expr[T])(
        rt: c.Expr[RefType[F]],
        v: c.Expr[Validate[T, P]]
    ): c.Expr[F[T, P]] = {
      val tValue: T = t.tree match {
        case Literal(Constant(value)) => value.asInstanceOf[T]
        case BigDecimalMatcher(value) => value.asInstanceOf[T]
        case BigIntMatcher(value)     => value.asInstanceOf[T]
        case _                        => abort(Resources.refineNonCompileTimeConstant)
      }
    

    List(1, 2, 3, 4, 5) is not a literal.

    For not literal values like List(1, 2, 3, 4, 5) there is refineV working at runtime

    val v1 = List(1, 2, 3, 4, 5)
    val v2 = List(1, 2, 3, 4, 5, 6)
    refineV[Size[Equal[5]]](v1) 
    // Right(List(1, 2, 3, 4, 5))
    refineV[Size[Equal[5]]](v2) 
    // Left(Predicate taking size(List(1, 2, 3, 4, 5, 6)) = 6 failed: Predicate failed: (6 == 5).)
    

    Fortunately you can run refineV at compile time

    object myAuto {
      implicit def compileTimeRefineV[T, P](t: T): T Refined P = 
        macro compileTimeRefineVImpl[T, P]
    
      def compileTimeRefineVImpl[T: c.WeakTypeTag, 
                                 P: c.WeakTypeTag](c: blackbox.Context)(t: c.Tree): c.Tree = {
        import c.universe._
        val pTyp = weakTypeOf[P]
        val tTyp = weakTypeOf[T]
        c.eval(c.Expr[Either[String, T Refined P]](c.untypecheck(
          q"_root_.eu.timepit.refined.`package`.refineV[$pTyp].apply[$tTyp]($t)"
        ))).fold(
          c.abort(c.enclosingPosition, _),
          _ => q"$t.asInstanceOf[_root_.eu.timepit.refined.api.Refined[$tTyp, $pTyp]]"
        )
      }
    }
    
    import myAuto._ // don't import eu.timepit.refined.auto._
    type Vec5 = List[Int] Refined Size[Equal[5]]
    val v1: Vec5 = List(1, 2, 3, 4, 5) // compiles
    // val v2: Vec5 = List(1, 2, 3, 4, 5, 6) 
      // Error: Predicate taking size(List(1, 2, 3, 4, 5, 6)) = 6 failed: Predicate failed: (6 == 5).
    

    If you just need statically-sized collection you can use shapeless.Sized

    https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0#collections-with-statically-known-sizes