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.
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