Type refinements in Scala but without using refined

368 Views Asked by At

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.

1

There are 1 best solutions below

4
On BEST ANSWER

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