2

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.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66

1 Answers1

4

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
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • it seems like this is what I want. One slight complication I would have is that the project in question uses Gradle instead of sbt and am not sure whether the settings link you provided is going to be of help... Hmmm, or should I just create a simple library and use sbt there? – Stefan Pavikevik May 19 '20 at 09:19
  • 1
    @StefanPavikevik you should set up your building tool (for example gradle). You can use sbt link as an exampe of settings (you should create two projects). – Dmytro Mitin May 19 '20 at 09:22
  • @StefanPavikevik Did you manage to set up Gradle to work with macros? – Dmytro Mitin May 26 '20 at 00:57
  • 1
    I did manage somehow by setting up a separate project in a separate directory with its own build.gradle file. I then added it as a dependency in the main project `implementation project("macros")` Thanks a lot for all the assistance! – Stefan Pavikevik May 26 '20 at 08:48