scalamacrosscala-macrosrefinement-typerefined

Type refinements in Scala but without using refined


I am trying to create a HexString type based on String which should fulfill the condition "that it contains only hexadecimal digits" and I would like to have the compiler typecheck it for me, if possible.

One obvious solution would be to use refined and write something like this:

type HexString = String Refined MatchesRegex[W.`"""^(([0-9a-f]+)|([0-9A-F]+))$"""`.T]
refineMV[MatchesRegex[W.`"""^(([0-9a-f]+)|([0-9A-F]+))$"""`.T]]("AF0")

Now, I have nothing against refined, it's that I find it a bit of an overkill for what I am trying to do (and have no idea whether I am going to use it in other places at all) and I am reluctant to import a library which I am not sure will be used more than once or twice overall and brings syntax that might look like magic (if not to me, to other devs on the team).

The best I can write with pure Scala code, on the other hand, is a value class with smart constructors, which is all fine and feels lightweight to me, except that I cannot do compile-time type checking. It looks something like this at the moment:

final case class HexString private (str: String) extends AnyVal {
  // ...
}

object HexString {
  def fromStringLiteral(literal: String): HexString = {
    def isValid(str: String): Boolean = "\\p{XDigit}+".r.pattern.matcher(str).matches

    if (isValid(literal)) HexString(literal)
    else throw new IllegalArgumentException("Not a valid hexadecimal string")
  }
}

For most of the codebase, runtime checking is enough as it is; however, I might need to have compile-time checking at some point and there seems to be no way of achieving it short of using refined.

If I can keep the code as localized and as understandable as possible, without introducing much magic, would it be possible to use a macro and instruct the compiler to test the RHS of assignment against a regex and depending on whether it matches or not, it would create an instance of HexString or spit a compiler error?

val ex1: HexString = "AF0" // HexString("AF0")
val ex2: HexString = "Hello World" // doesn't compile

Other than ADT traversal and transformation programs I've written using Scala meta, I don't really have experience with Scala macros.


Solution

  • If you want fromStringLiteral to work at compile time you can make it a macro (see sbt settings)

    import scala.language.experimental.macros
    import scala.reflect.macros.blackbox
    
    def fromStringLiteral(literal: String): HexString = macro fromStringLiteralImpl
    
    def fromStringLiteralImpl(c: blackbox.Context)(literal: c.Tree): c.Tree = {
      import c.universe._
    
      val literalStr = literal match {
        case q"${s: String}" => s
        case _ => c.abort(c.enclosingPosition, s"$literal is not a string literal")
      }
    
      if (isValid(literalStr)) q"HexString($literal)"
      else c.abort(c.enclosingPosition, s"$literalStr is not a valid hexadecimal string")
    }
    

    Then

    val ex1: HexString = HexString.fromStringLiteral("AF0") // HexString("AF0")
    //val ex2: HexString = HexString.fromStringLiteral("Hello World") // doesn't compile
    

    If you want this to work like

    import HexString._
    val ex1: HexString = "AF0" // HexString("AF0")
    //val ex2: HexString = "Hello World" // doesn't compile
    

    then additionally you can make fromStringLiteral an implicit conversion

    implicit def fromStringLiteral(literal: String): HexString = macro fromStringLiteralImpl