How to encode a constraint on the format of String values
As I frequently observe and how I often implement a name attribute, is to simply model it as String.
What now, if the name has to follow a certain syntax, i.e. format? In Java I probably would define a constructor with a check on its arguments, something like:
public Name(str: String) {
    if (str == null) throw new IllegalArgumentException("Str must not be null.");
    if (!str.matches("name format expressed as regex")) throw new IllegalArgumentException("Str must match 'regex' but was " + str);
    this.str = str;
}
In Scala I came up with the following solution:
import StdDef.Str
import StdDef.Bol
import StdDef.?
import scala.util.parsing.combinator.RegexParsers
final case class Name private (pfx: ?[Str] = None, sfx: Str) {
  override def toString = pfx.mkString + sfx
}
object Name extends RegexParsers {
  implicit def apply(str: Str): Name = parseAll(syntax, str) match {
    case Success(res, _) => Name(res._1, res._2)
    case rej: NoS开发者_如何转开发uccess => error(rej.toString)
  }
  lazy val syntax = (prefix ?) ~! suffix
  lazy val prefix = (("x" | "X") ~! hyph) ^^ { case a ~ b => a + b }
  lazy val suffix = alpha ~! (alpha | digit | hyph *) ^^ { case a ~ b => a + b.mkString }
  lazy val alpha: Parser[Str] = """\p{Alpha}""".r
  lazy val digit: Parser[Str] = """\p{Digit}""".r
  lazy val hyph: Parser[Str] = "-"
  override lazy val skipWhitespace = false
}
My intents here are:
- Compose a Namefrom its natural representation, i.e. aStringvalue
- Check whether its natural representation forms a valid Nameat construction time.
- Disallow any other construction than through the factory method apply:(str:Str)Str.
- Make the construction from its natural representation implicit, e.g. val a: Name = "ISBN 978-0-9815316-4-9".
- Decompose a Nameinto its parts according to its syntactical elements.
- Have errors being thrown with messages, such as:
===
--
^
[1.3] error: string matching regex `\p{Alpha}' expected but end of source found
I would like to know what solutions you come up with.
After giving the topic some more thoughts, I am currently taking the following approach.
Token.scala:
abstract class Token {
  val value: Str
}
object Token {
  def apply[A <: Token](ctor: Str => A, syntax: Regex) = (value: Str) => value match {
    case syntax() => ctor(value)
    case _ => error("Value must match '" + syntax + "' but was '" + value + "'.")
  }
}
Tokens.scala:
final case class Group private (val value: Str) extends Token
final case class Name private (val value: Str) extends Token
trait Tokens {
  import foo.{ bar => outer }
  val Group = Token(outer.Group, """(?i)[a-z0-9-]++""".r)
  val Name = Token(outer.Name, """(?i)(?:x-)?+[a-z0-9-]++""".r)
}
Given that you'd be comfortable using a regex in Java, it seems like overkill to then try and solve the same problem with a parser in Scala.
Stick with what you know here, but add a Scala twist to clean up the solution a bit. Regexes in Scala also define extractors, allowing them to be used in a pattern match:
//triple-quote to make escaping easier, the .r makes it a regex
//Note how the value breaks normal naming conventions and starts in uppercase
//This is to avoid backticks when pattern matching
val TestRegex = """xxyyzz""".r
class Name(str: String) {
  str match {
    case Null => throw new IllegalArgumentException("Str must not be null")
    case TestRegex => //do nothing
    case _ => throw new IllegalArgumentException(
      "Str must match 'regex' but was " + str)
  }
}
disclaimer: I didn't actually test this code, it may contain typos
 
         加载中,请稍侯......
 加载中,请稍侯......
      
精彩评论