8
\$\begingroup\$

I had designed a simple case class that looks like this:

case class StreetSecondary(designator: String, value: Option[String])

I needed to add validation (to prevent invalid instances from ever being constructed). So, I then modified the class to look like this:

case class StreetSecondary(designator: String, value: Option[String]) {
  require(
      designator.forall(char => ((char == '#') || char.isLetter))
    , s"designator [$designator] must only contain characters from [#,A-Za-z]"
  )
  require(
      value.isEmpty || (value.get.head != ' ')
    , s"if defined, value [${value.get}] must not start with a space"
  )
}

However, this means that client code using this case class ends up having to deal with any failures as exceptions (bad FP code smell). Additionally, there is no way to know if more than one test failed as the constructor aborts at the first failed condition. It is preferable to return a list of failures as opposed to having to repeatedly hitting the constructor to slowly sift through all of the possible validation exceptions.

So, to produce this list of failed validations, I decided to define an explicit companion object to the case class. This has the unfortunate consequence of blowing away the scala compiler provided implicit companion object (please see this post for more details). So, here is what the implementation now looks like:

object StreetSecondary extends ((String, Option[String]) => StreetSecondary) {
  override def toString = getClass.getName.split("""\$""").reverse.dropWhile(!_.take(1).head.isLetter).head
  def validate(designator: String, value: Option[String]): List[RuntimeException] = {
    val exceptions: List[Option[RuntimeException]] =
      List(
          if (!designator.forall(char => ((char == '#') || char.isLetter)))
            Some(new IllegalStateException(s"designator [$designator] must only contain characters from [#,A-Za-z]"))
          else None
        , if (value.isDefined && (value.get.head == ' '))
            Some(new IllegalStateException(s"if defined, value [${value.get}] must not start with a space"))
          else None
      )
    exceptions.flatten
  }
}
case class StreetSecondary(designator: String, value: Option[String]) {
  {
    val exceptions: List[RuntimeException] = StreetSecondary.validate(designator, value)
    require(exceptions.isEmpty, s"found validation exceptions [${exceptions.mkString(",")}]")
  }
}

This now allows the client to call the StreetSecondary.validate method prior to attempting to instantiate the case class. And the case class itself is still using require to guard against instantiating invalid instances

At this point, offering a single call point which either returned a valid instance or returned the list of failed validations would be nice. And the Either class is perfect for this. Here's what the code looks like:

object StreetSecondary extends ((String, Option[String]) => StreetSecondary) {
  override def toString = getClass.getName.split("""\$""").reverse.dropWhile(!_.take(1).head.isLetter).head
  def validate(designator: String, value: Option[String]): List[RuntimeException] = {
    val exceptions: List[Option[RuntimeException]] =
      List(
          if (!designator.forall(char => ((char == '#') || char.isLetter)))
            Some(new IllegalStateException(s"designator [$designator] must only contain characters from [#,A-Za-z]"))
          else None
        , if (value.isDefined && (value.get.head == ' '))
            Some(new IllegalStateException(s"if defined, value [${value.get}] must not start with a space"))
          else None
      )
    exceptions.flatten
  }
  def construct(designator: String, value: Option[String]): Either[List[RuntimeException], StreetSecondary] = {
    val exceptions = validate(designator, value)
    if (exceptions.nonEmpty) Left(exceptions)
    else Right(StreetSecondary(designator, value))
  }
}
case class StreetSecondary(designator: String, value: Option[String]) {
  {
    val exceptions: List[RuntimeException] = StreetSecondary.validate(designator, value)
    require(exceptions.isEmpty, s"found validation exceptions [${exceptions.mkString(",")}]")
  }
}

I had to use the method name construct instead of apply due to an idiosyncrasy around not allowing an explicit companion object to have an apply with the same parameter type signature as the case class's primary constructor.

And because I extensively use tuples in my client code, I have one final requirement to enable tuple parameter signature versions of both the validate and construct methods. Additionally, I would like to have an apply method using the tuple parameter signature. The code I produced looks like this:

type StreetSecondaryParameters = (String, Option[String])
object StreetSecondary extends ((String, Option[String]) => StreetSecondary) {
  //replace the toString implementation from FunctionN
  override def toString = getClass.getName.split("""\$""").reverse.dropWhile(!_.take(1).head.isLetter).head
  def validate(values: StreetSecondaryParameters): List[RuntimeException] = {
    val (designator, value) = values
    val exceptions: List[Option[RuntimeException]] =
      List(
          if (!designator.forall(char => ((char == '#') || char.isLetter)))
            Some(new IllegalStateException(s"designator [$designator] must only contain characters from [#,A-Za-z]"))
          else None
        , if (value.isDefined && (value.get.head == ' '))
            Some(new IllegalStateException(s"if defined, value [${value.get}] must not start with a space"))
          else None
      )
    exceptions.flatten
  }
  def construct(values: StreetSecondaryParameters): Either[List[RuntimeException], StreetSecondary] = {
    val exceptions = validate(values)
    if (exceptions.nonEmpty) Left(exceptions)
    else Right(StreetSecondary.tupled(values))
  }
  def validate(designator: String, value: Option[String]): List[RuntimeException] =
    validate((designator, value))
  def construct(designator: String, value: Option[String]): Either[List[RuntimeException], StreetSecondary] =
    construct((designator, value))
  def apply(values: StreetSecondaryParameters): StreetSecondary = {
    StreetSecondary.tupled(values)
  }
}
case class StreetSecondary(designator: String, value: Option[String]) {
  {
    val exceptions: List[RuntimeException] = StreetSecondary.validate(designator, value)
    require(exceptions.isEmpty, s"found validation exceptions [${exceptions.mkString(",")}]")
  }
}

My challenge is that this is quite a bit of boilerplate, even for Scala (with plenty of DRY violations). And I must do this for each case class I need (and I need dozens). So, before I write a Scala code generator to spew out the code I need from templates, I would like to know if I am missing some sort of obvious ways to reduce this boilerplate without losing any of the key functionality I described in my pathway above.

\$\endgroup\$
0

4 Answers 4

2
\$\begingroup\$

Minor point: designator.forall(char => ((char == '#') || char.isLetter)) would look more normal as a regex.

I don't think you should throw exceptions from the constructor with require. Exceptions should be for exceptional circumstances and a failed validation is not an exceptional circumstance. You can make the case class constructor private and only rely on the contructor methods from the object. You don't want the users to be able to create an unvalidated instance.

I tried to find ways to shorten/generalize your code, but I could not think of anything. Tuples are not monads and there is not much to do with them.

EDIT as reply to comment:

This does run:

  case class A private (i: Int)
  object A {
    def create(i: Int) = A(i)
  }
  println(A.create(3))  

See also this stackoverflow post.

I guess "advanced" is relative to experience:

val nameRegex = """\A[#a-zA-Z]*\Z""".r
def isValidName(word: String): Boolean = nameRegex.findFirstIn(word).isDefined

I did have to mess around a bit to get it right. Note that it accepts the empty string, as did your original implementation. If you don't want to accept the empty string, just change * to + in the regexp.

\$\endgroup\$
6
  • \$\begingroup\$ Tysvm for your feedback. I am not sure how to create a regex with that particular solution (that would be some advanced regex). As to not having the constructor throw exceptions, how would you change that? I am not able to make the case class constructor private and then create a signature matching apply (constructor) in the explicit companion object (the compiler yells about it - sure seems like this was a design oversight). And to successfully do pattern matching, I need that particular constructor, right? Hence, the compromise of leaving it public and guarding it with the requires block. \$\endgroup\$ Aug 21, 2014 at 13:29
  • \$\begingroup\$ See my edit relating to those two points. \$\endgroup\$
    – toto2
    Aug 21, 2014 at 17:01
  • \$\begingroup\$ The StackOverflow post is dealing with non-case classes. Doesn't your solution block pattern matching for the case class A? \$\endgroup\$ Aug 21, 2014 at 19:03
  • \$\begingroup\$ I don't think your regex solves the problem. Here's the actual text from getClass.getName, "org.public_domain.address.A$A134$A$A134$StreetSecondary$4$". The desired string is the second from the last value where "$" is used as a separator. All of that extra nonsense is present due to the context being the Scala Worksheet (although it remains noisy once in a regular package structure hanging out in an in place function in an in place object. IOW, I don't get to pick and choose where the name appears in getClass.getName. \$\endgroup\$ Aug 21, 2014 at 19:08
  • \$\begingroup\$ Again, regarding the regex, assuming Java simple names (not the huge Unicode domain that Scala actually allows), I believe the first character must be a letter or an underscore. And after the first character, valid characters are any of letter (both cases), digits and underscore. It was all this crap that had me skip to the much simpler rule of: use $ as a separator, reverse the string, and find the first identifier that started with a valid letter. I probably need to expand it to also include underscore. \$\endgroup\$ Aug 21, 2014 at 19:31
1
\$\begingroup\$

SIDENOTE:
While it is unusual to do so, I am adding a second answer. Per the meta site, they specify a strong preference to avoid editing an existing answer if there are any comments already attached to it.


After quite extensive additional research and testing, I have finally arrived at my "ideal" solution for defining and using ADTs (Abstract Data Type) via case classes. While I would still like to reduce the DRY (Don't Repeat Yourself) further, the resulting design feels very satisfying. Discovering some of the details on this journey was quite challenging.

Given the last code snippit from the original post, I have made some fundamental changes aligning to the following goals:

A. Reducing boilerplate
B. Retaining all of the normal/expected case class functionality (including efficiency, i.e. internally avoiding calling the validate method redundantly)
C. Restoring functionality in explicit companion object (lost when the implicit companion object was replaced)
D. Forcing all instantiation of the case class through a single point in the explicit companion object; i.e. the construct(designator: String, value: Option[String]): Either[List[RuntimeException], SuccessfullyConstructedCaseClassInstance] method
E. Adding an FP style validate method returning List[RuntimeException]
F. Adding an FP style construct method returning Either[List[RuntimeException], SuccessfullyConstructedCaseClassInstance]
G. Adding a tuple version for each of the apply, validate and construct methods
H. Facilitate automated code generation (moving the custom exceptions into the specialized produceValidationRuntimeExceptions method)
I. Adding an FP style constructCopy method returning Either[List[RuntimeException], SuccessfullyConstructedCaseClassInstance]
J. Generating a set of "compilation and validation test cases" to thoroughly cover the entire desired client functionality surface

Here is what the code now looks like:

type StreetSecondaryParameters = (String, Option[String])
type StreetSecondaryEither = Either[List[RuntimeException], StreetSecondary]
object StreetSecondary extends PartialFunction[StreetSecondaryParameters, StreetSecondary] {
  override def toString =
    getClass.getName.split("\\$").reverse.dropWhile(x => {val char = x.take(1).head; !((char == '_') || char.isLetter)}).head
  def apply(designator: String, value: Option[String]): StreetSecondary =
    construct(designator, value) match {
      case Right(streetSecondary) => streetSecondary
      case Left(exceptions) =>
        throw new IllegalStateException(s"validate failed for designator [$designator] and value [${value.getOrElse("<None>")}] - exceptions [${exceptions.mkString("|")}]")
    }
  def apply(streetSecondaryParameters: StreetSecondaryParameters): StreetSecondary =
    apply(streetSecondaryParameters._1, streetSecondaryParameters._2)
  def isDefinedAt(streetSecondaryParameters: StreetSecondaryParameters): Boolean =
    validate(streetSecondaryParameters).isEmpty
  def validate(streetSecondaryParameters: StreetSecondaryParameters): List[RuntimeException] =
    validate(streetSecondaryParameters._1, streetSecondaryParameters._2)
  def validate(designator: String, value: Option[String]): List[RuntimeException] =
    produceValidationRuntimeExceptions(designator, value)
  def construct(streetSecondaryParameters: StreetSecondaryParameters): StreetSecondaryEither =
    construct(streetSecondaryParameters._1, streetSecondaryParameters._2)
  def construct(designator: String, value: Option[String]): StreetSecondaryEither = {
    val exceptions = validate(designator, value)
    if (exceptions.isEmpty)
      Right(new StreetSecondary(designator, value) {/* intentionally empty implementation */})
    else
      Left(exceptions)
  }
  def produceValidationRuntimeExceptions(designator: String, value: Option[String]): List[RuntimeException] =
      List(
        if (!designator.forall(char => ((char == '#') || char.isLetter)))
          Some(new IllegalStateException(s"designator [$designator] must only contain characters from [#,A-Za-z]"))
        else None
      , if (value.isDefined && (value.get.head == ' '))
          Some(new IllegalStateException(s"if defined, value [${value.get}] must not start with a space"))
        else None
      ).flatten
  def apply(values: String): StreetSecondary = {
    def parseToParameters(values: String): StreetSecondaryParameters = {
      val (designator, value) = values.span(_ != ' ')
      (designator, Option(value.trim).filter(_.nonEmpty))
    }
    val streetSecondaryParameters = parseToParameters(values)
    apply(streetSecondaryParameters._1, streetSecondaryParameters._2)
  }
}
abstract case class StreetSecondary private[StreetSecondary] (designator: String, value: Option[String]) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    StreetSecondary.apply(designator, value)
  def copy(designator: String = designator, value: Option[String] = value): StreetSecondary =  //to ensure validation, must "override" copy to use explicit companion object apply method
    StreetSecondary.apply(designator, value)
  def constructCopy(designator: String = designator, value: Option[String] = value): StreetSecondaryEither =
    StreetSecondary.construct(designator, value)
}

Related to goal J above, here are the test cases (intended for use with the Scala Worksheet; just copy and paste):

  //won't compile as the constructor is private, which is exactly the desired outcome
  //val instantiatesViaNew =
  //  List(
  //      Try(new StreetSecondary("Upper", None)) {/* intentionally empty implementation */})
  //    , Try(new StreetSecondary("#", Some("331"))) {/* intentionally empty implementation */})
  //    , Try(new StreetSecondary("Upper!", None)) {/* intentionally empty implementation */})
  //    , Try(new StreetSecondary("#!", Some("331"))) {/* intentionally empty implementation */})
  //    , Try(new StreetSecondary("#", Some(" 331"))) {/* intentionally empty implementation */})
  //    , Try(new StreetSecondary("#!", Some(" 331"))) {/* intentionally empty implementation */})
  //  )
  def instantiatesViaApplyImplicit =
    List(
        Try(StreetSecondary("Upper", None))
      , Try(StreetSecondary("#", Some("331")))
      , Try(StreetSecondary("Upper!", None))
      , Try(StreetSecondary("#!", Some("331")))
      , Try(StreetSecondary("#", Some(" 331")))
      , Try(StreetSecondary("#!", Some(" 331")))
    )
  val instantiatesViaApplyExplicit =
    List(
        Try(StreetSecondary.apply("Upper", None))
      , Try(StreetSecondary.apply("#", Some("331")))
      , Try(StreetSecondary.apply("Upper!", None))
      , Try(StreetSecondary.apply("#!", Some("331")))
      , Try(StreetSecondary.apply("#", Some(" 331")))
      , Try(StreetSecondary.apply("#!", Some(" 331")))
    )
  val instantiatesViaCopy = {
    val streetSecondary = StreetSecondary.apply("Lower", None)
    List(
        Try(streetSecondary.copy(designator = "Upper"))
      , Try(streetSecondary.copy(designator = "Upper", value = None))
      , Try(streetSecondary.copy(designator = "#", value = Some("331")))
      , Try(streetSecondary.copy(value = None))
      , Try(streetSecondary.copy(value = Some("331")))
      , Try(streetSecondary.copy(designator = "Upper!"))
      , Try(streetSecondary.copy(designator = "Upper!", value = None))
      , Try(streetSecondary.copy(designator = "#!", value = Some("331")))
      , Try(streetSecondary.copy(designator = "#", value = Some(" 331")))
      , Try(streetSecondary.copy(designator = "#!", value = Some(" 331")))
      , Try(streetSecondary.copy(value = Some(" 331")))
    )
  }
  val instantiatesViaConstruct =
    List(
        Try(StreetSecondary.construct("Upper", None))
      , Try(StreetSecondary.construct("#", Some("331")))
      , Try(StreetSecondary.construct("Upper!", None))
      , Try(StreetSecondary.construct("#!", Some("331")))
      , Try(StreetSecondary.construct("#", Some(" 331")))
      , Try(StreetSecondary.construct("#!", Some(" 331")))
    )
  val tuples =
    List(
        ("Upper", None)
      , ("#", Some("331"))
      , ("Upper!", None)
      , ("#!", Some("331"))
      , ("#", Some(" 331"))
      , ("#!", Some(" 331"))
    )
  val instantiatesViaApplyImplicitTuple =
    tuples.map(x => Try(StreetSecondary(x)))
  val instantiatesViaApplyExplicitTuple =
    tuples.map(x => Try(StreetSecondary.apply(x)))
  //
  //val newSuccesses = instantiatesViaNew.forall(_.isSuccess)
  val applyImplicitSuccesses = instantiatesViaApplyImplicit.forall(_.isSuccess)
  val applyExplicitSuccesses = instantiatesViaApplyExplicit.forall(_.isSuccess)
  val copySuccesses = instantiatesViaCopy.forall(_.isSuccess)
  val constructSuccesses = instantiatesViaConstruct.forall(_.isSuccess)
  val applyImplicitTupleSuccesses = instantiatesViaApplyImplicitTuple.forall(_.isSuccess)
  val applyExplicitTupleSuccesses = instantiatesViaApplyExplicitTuple.forall(_.isSuccess)
  //val newSuccessesDetail = instantiatesViaNew.mkString(",")
  val applyImplicitSuccessesDetail = instantiatesViaApplyImplicit.mkString(",")
  val applyExplicitSuccessesDetail = instantiatesViaApplyExplicit.mkString(",")
  val copySuccessesDetail = instantiatesViaCopy.mkString(",")
  val constructSuccessesDetail = instantiatesViaConstruct.mkString(",")
  val applyImplicitTupleSuccessesDetail = instantiatesViaApplyImplicitTuple.mkString(",")
  val applyExplicitTupleSuccessesDetail = instantiatesViaApplyExplicitTuple.mkString(",")

Here are some highlights about the code itself related to the original goals; i.e. the trickly little things that end up mattering quite a bit:

  1. The two type statements (type StreetSecondaryParameters = ... and type StreetSecondaryEither = ...) meet goal A
  2. The extends PartialFunction[... meets goal C (providing a more specific version of Function2 which would have been provided by the compiler generated version)
  3. The override def toString = meets goal C
  4. The def apply(designator: String, value: Option[String]): StreetSecondary = meets goals B and C (is implemented in terms of E and F)
  5. The def isDefinedAt(...): Boolean = meets side-effect of implemeting goal C via a PartialFunction
  6. Both validate and construct methods are meeting goals E, F and G
  7. For construct(...): StreetSecondaryEither, the Right(new StreetSecondary(...) {/* intentionally empty implementation */}) is leveraging the fact the case class is declared abstract
  8. The def produceValidationRuntimeExceptions(... meets goal H
  9. There are two declarations in abstract case class StreetSecondary private[StreetSecondary] (...) { which meets goal D;
    1. The use of abstract prevents the compiler from automatically generating the apply method
    2. The private[StreetSecondary] prevents all other classes and objects from calling the case class's primary constructor
  10. The def copy(... meets goal B (a ticket has been opened against the Scala compiler to address an anomaly around having to explicitly manually implement this method)
  11. The def constructCopy(... meets goal I

A big thank you to both toto2 and Randall Schultz for helping me push through the noise related to making the case class constructor private. A huge thank you to Martin Odersky for pointing out the obscure trick (to me) of declaring the case class as abstract to cause the compiler to not automatically generate an apply method.

\$\endgroup\$
2
  • \$\begingroup\$ And here is a simpler case class "pattern" upon which this answer was built: stackoverflow.com/questions/5827510/… \$\endgroup\$ Aug 29, 2014 at 18:39
  • \$\begingroup\$ Minor addition: the require keyword in case classes is extracted by akka-http, providing some out-of-the-box validation: doc.akka.io/docs/akka-http/current/routing-dsl/… Yet it's not so great as a first thought it was: it'll only work when using akka-http (and possibly only with spray-json) through to some magic. When using outside you'll end up with an exception anyway. \$\endgroup\$
    – Gerbrand
    Nov 27, 2019 at 10:09
0
\$\begingroup\$

After reviewing feedback from both @toto2 and @RandallSchultz, and after working through other issues related to clients using the Scala compiler's provided copy method, I have made a number of changes:

  1. Updated the explicit companion object's class name generator to properly account for class names starting with an underscore ("_").
  2. Added a produceValidationRuntimeExceptions method to separate validation implementation from validation interface - simplifies future automated code generation.
  3. Added private to the case class constructor thereby requiring all instances be created via the explicit companion object's construct methods - eases facilitating future instance caching strategies.
  4. Added a constructCopy method to the case class to safely reproduce the compiler generated copy method's functionality - properly routes instantiation through the explicit companion object's construct method.
  5. Added a copy method to the case class to suppress the compiler generated copy method.
  6. Implemented the copy method with an UnsupportedOperationException directing the client to use the constructCopy method.

Here is what the code now looks like with these changes:

type StreetSecondaryParameters = (String, Option[String])
object StreetSecondary extends ((String, Option[String]) => StreetSecondary) {
  override def toString =
    getClass.getName.split("\\$").reverse.dropWhile(x => {val char = x.take(1).head; !((char == '_') || char.isLetter)}).head
  def validate(values: StreetSecondaryParameters): List[RuntimeException] =
    produceValidationRuntimeExceptions(values)
  def construct(values: StreetSecondaryParameters): Either[List[RuntimeException], StreetSecondary] = {
    val exceptions = validate(values)
    if (exceptions.nonEmpty) Left(exceptions)
    else Right(StreetSecondary.tupled(values))
  }
  def validate(designator: String, value: Option[String]): List[RuntimeException] =
    validate((designator, value))
  def construct(designator: String, value: Option[String]): Either[List[RuntimeException], StreetSecondary] =
    construct((designator, value))
  def apply(values: StreetSecondaryParameters): StreetSecondary =
    StreetSecondary.tupled(values)
  //
  def produceValidationRuntimeExceptions(values: StreetSecondaryParameters): List[RuntimeException] = {
    val (designator, value) =
      values
    List(
      if (!designator.forall(char => ((char == '#') || char.isLetter)))
        Some(new IllegalStateException(s"designator [$designator] must only contain characters from [#,A-Za-z]"))
      else None
    , if (value.isDefined && (value.get.head == ' '))
        Some(new IllegalStateException(s"if defined, value [${value.get}] must not start with a space"))
      else None
    ).flatten
  }
}
case class StreetSecondary private (designator: String, value: Option[String]) {
  def copy(designator: String = designator, value: Option[String] = value): StreetSecondary =
    throw new UnsupportedOperationException("use constructCopy instead")
  def constructCopy(designator: String = designator, value: Option[String] = value): Either[List[RuntimeException], StreetSecondary] =
    StreetSecondary.construct(designator, value)
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ If you really want to start doing code generation, maybe you could look at Scala macros. I believe it is equivalent to code generation at some level. Maybe you could deal with tuples as monads. \$\endgroup\$
    – toto2
    Aug 22, 2014 at 15:14
0
\$\begingroup\$

SIDENOTE:
Again, while I'd prefer to not do so, I am adding a third answer (as opposed to editing one of the two existing ones above). Per the meta site, they specify a very strong preference to avoid my editing an existing answer if there are any comments already attached to it.


I decided to revisit this case class pattern recently, when I decided to add caching (via ScalaCache). I started from scratch using Scala 2.11.7 and reimplemented the entire pattern hoping to even further reduce boilerplate. I discovered several ways to simplify things. My intention is to eventually turn it into a Scala macro. Here's what the new code looks like:

object StreetSecondary extends ((String, Option[String]) => StreetSecondary) {
  type Parameters = (String, Option[String])

  val cache =
    CacheBuilder
      .newBuilder()
      .maximumSize(10000L)
      .expireAfterAccess(30, TimeUnit.SECONDS)
      .build[String, Object]
  implicit val scalaCache: ScalaCache = ScalaCache(GuavaCacheWrapper(cache))

  def apply(designator: String, value: Option[String] = None): StreetSecondary =
    validate(designator, value) match {
      case Some(runtimeExceptionsException) =>
        throw runtimeExceptionsException
      case None =>
        create(designator, value)
    }

  def apply(parameters: Parameters): StreetSecondary =
    StreetSecondary(parameters._1, parameters._2)

  def tryValidatedFactory(designator: String, value: Option[String] = None): Try[() => StreetSecondary] =
    validate(designator, value) match {
      case Some(runtimeExceptionsException) =>
        Failure(runtimeExceptionsException)
      case None =>
        Success(
          new (() => StreetSecondary) {
            def apply(): StreetSecondary =
              create(designator, value)
          }
        )
    }

  def tryValidatedFactory(parameters: Parameters): Try[() => StreetSecondary] =
    tryValidatedFactory(parameters._1, parameters._2)

  def validate(designator: String, value: Option[String] = None): Option[RuntimeExceptionsException] =
    RuntimeExceptionsException(clientProvidedValidateImpl(designator, value))

  def validate(parameters: Parameters): Option[RuntimeExceptionsException] =
    validate(parameters._1, parameters._2)

  def clientProvidedValidateImpl(designator: String, value: Option[String]): List[RuntimeException] = {
    def validateInRange(name: String, upperBound: Double, value: Double): Option[IllegalArgumentException] =
      if (!(-upperBound <= value))
        Some(new IllegalArgumentException(s"$name [$value] must be greater than or equal to ${-upperBound}"))
      else
        if (!(value < upperBound))
          Some(new IllegalArgumentException(s"$name [$value] must be less than $upperBound"))
        else
          None
    List(
        if (!designator.forall(char => ((char == '#') || char.isLetter)))
          Some(new IllegalStateException(s"designator [$designator] must only contain characters from [#,A-Za-z]"))
        else
          None
      , value.flatMap(
          valueGet =>
            if (valueGet.head == ' ')
              Some(new IllegalStateException(s"if defined, value [$valueGet] must not start with a space"))
            else
              None
        )
    ).flatten
  }

  private def create(designator: String, value: Option[String]): StreetSecondary = memoize {
    new StreetSecondary(designator, value) {
      private def readResolve(): Object =
        StreetSecondary(designator, value)

      def copy(designator: String = designator, value: Option[String] = value): StreetSecondary =
        StreetSecondary(designator, value)

      val tuple: StreetSecondary.Parameters =
        (designator, value)
    }
  }
}
abstract case class StreetSecondary private[StreetSecondary] (designator: String, value: Option[String]) { //example J
  def copy(designator: String = designator, value: Option[String] = value): StreetSecondary
  def tuple: StreetSecondary.Parameters
}

Here are the test cases (intended for use with Scala WorkSheet; just copy and paste):

val tryDesignator1 = Try(StreetSecondary("Upper"))
val tryDesignator2 = Try(StreetSecondary("Upper", None))
val tryBoth1 = Try(StreetSecondary("Upper", Some("331")))
val tryInvalidDesignator = Try(StreetSecondary("Upper!", None))
val tryInvalidBoth1 = Try(StreetSecondary("Upper!", Some("331")))
val tryInvalidBoth2 = Try(StreetSecondary("Upper", Some(" 331")))
val tryInvalidBoth3 = Try(StreetSecondary("Upper!", Some(" 331")))
val tryBoth2 = Try(StreetSecondary("Ste", Some("100")))
//
//Note: these won't compile as the constructor is private - desired effect
//val tryNewDesignator = Try(new StreetSecondary("Upper", None) { /* implementation intentionally left blank */})
//val tryNewBoth = Try(new StreetSecondary("Upper", Some("331")) { /* implementation intentionally left blank */})
//val tryNewInvalidDesignator = Try(new StreetSecondary("Upper!", None) { /* implementation intentionally left blank */})
//val tryNewInvalidBoth1 = Try(new StreetSecondary("Upper!", Some("331")) { /* implementation intentionally left blank */})
//val tryNewInvalidBoth2 = Try(new StreetSecondary("Upper!", Some(" 331")) { /* implementation intentionally left blank */})
//val tryNewInvalidBOth3 = Try(new StreetSecondary("Upper", Some(" 331")) { /* implementation intentionally left blank */})
//
val tryCopyDesignator = Try(tryBoth2.get.copy(designator = "Upper"))
val tryCopyValue = Try(tryBoth2.get.copy(value = Some("331")))
val tryCopyBoth1 = Try(tryBoth2.get.copy(designator = "Upper", value = None))
val tryCopyBoth2 = Try(tryBoth2.get.copy(designator = "Upper", value = Some("331")))
val tryCopyInvalidDesignator = Try(tryBoth2.get.copy(designator = "Upper!", value = None))
val tryCopyInvalidBoth1 = Try(tryBoth2.get.copy(designator = "Upper!", value = Some("331")))
val tryCopyInvalidBoth2 = Try(tryBoth2.get.copy(designator = "Upper", value = Some(" 331")))
val tryCopyInvalidBoth3 = Try(tryBoth2.get.copy(designator = "Upper!", value = Some(" 331")))
//
val validateDesignator1 = StreetSecondary.validate("Upper")
val validateDesignator2 = StreetSecondary.validate("Upper", None)
val validateBoth1 = StreetSecondary.validate("Upper", Some("331"))
val validateInvalidDesignator = StreetSecondary.validate("Upper!", None)
val validateInvalidBoth1 = StreetSecondary.validate("Upper!", Some("331"))
val validateInvalidBoth2 = StreetSecondary.validate("Upper", Some(" 331"))
val validateInvalidBoth3 = StreetSecondary.validate("Upper!", Some(" 331"))
val validateBoth2 = StreetSecondary.validate("Ste", Some("100"))
//
val tryValidatedFactoryDesignator1 = StreetSecondary.tryValidatedFactory("Upper").map(_.apply())
val tryValidatedFactoryDesignator2 = StreetSecondary.tryValidatedFactory("Upper", None).map(_.apply())
val tryValidatedFactoryBoth1 = StreetSecondary.tryValidatedFactory("Upper", Some("331")).map(_.apply())
val tryValidatedFactoryInvalidDesignator = StreetSecondary.tryValidatedFactory("Upper!", None).map(_.apply())
val tryValidatedFactoryInvalidBoth1 = StreetSecondary.tryValidatedFactory("Upper!", Some("331")).map(_.apply())
val tryValidatedFactoryInvalidBoth2 = StreetSecondary.tryValidatedFactory("Upper", Some(" 331")).map(_.apply())
val tryValidatedFactoryInvalidBoth3 = StreetSecondary.tryValidatedFactory("Upper!", Some(" 331")).map(_.apply())
val tryValidatedFactoryBoth2 = StreetSecondary.tryValidatedFactory("Ste", Some("100")).map(_.apply())
//
val tryTupledDesignator2 = Try(StreetSecondary.tupled(("Upper", None)))
val tryTupledBoth1 = Try(StreetSecondary.tupled(("Upper", Some("331"))))
val tryTupledInvalidDesignator = Try(StreetSecondary.tupled(("Upper!", None)))
val tryTupledInvalidBoth1 = Try(StreetSecondary.tupled(("Upper!", Some("331"))))
val tryTupledInvalidBoth2 = Try(StreetSecondary.tupled(("Upper", Some(" 331"))))
val tryTupledInvalidBoth3 = Try(StreetSecondary.tupled(("Upper!", Some(" 331"))))
val tryTupledBoth2 = Try(StreetSecondary.tupled(("Ste", Some("100"))))
//
val tryApplyTupleDesignator2 = Try(StreetSecondary(("Upper", None)))
val tryApplyTupleBoth1 = Try(StreetSecondary(("Upper", Some("331"))))
val tryApplyTupleInvalidDesignator = Try(StreetSecondary(("Upper!", None)))
val tryApplyTupleInvalidBoth1 = Try(StreetSecondary(("Upper!", Some("331"))))
val tryApplyTupleInvalidBoth2 = Try(StreetSecondary(("Upper", Some(" 331"))))
val tryApplyTupleInvalidBoth3 = Try(StreetSecondary(("Upper!", Some(" 331"))))
val tryApplyTupleBoth2 = Try(StreetSecondary(("Ste", Some("100"))))
//
val tryValidatedFactoryTupleDesignator2 = StreetSecondary.tryValidatedFactory(("Upper", None)).map(_.apply())
val tryValidatedFactoryTupleBoth1 = StreetSecondary.tryValidatedFactory(("Upper", Some("331"))).map(_.apply())
val tryValidatedFactoryTupleInvalidDesignator = StreetSecondary.tryValidatedFactory(("Upper!", None)).map(_.apply())
val tryValidatedFactoryTupleInvalidBoth1 = StreetSecondary.tryValidatedFactory(("Upper!", Some("331"))).map(_.apply())
val tryValidatedFactoryTupleInvalidBoth2 = StreetSecondary.tryValidatedFactory(("Upper", Some(" 331"))).map(_.apply())
val tryValidatedFactoryTupleInvalidBoth3 = StreetSecondary.tryValidatedFactory(("Upper!", Some(" 331"))).map(_.apply())
val tryValidatedFactoryTupleBoth2 = StreetSecondary.tryValidatedFactory(("Ste", Some("100"))).map(_.apply())
//
val validateTupleDesignator2 = StreetSecondary.validate(("Upper", None))
val validateTupleBoth1 = StreetSecondary.validate(("Upper", Some("331")))
val validateTupleInvalidDesignator = StreetSecondary.validate(("Upper!", None))
val validateTupleInvalidBoth1 = StreetSecondary.validate(("Upper!", Some("331")))
val validateTupleInvalidBoth2 = StreetSecondary.validate(("Upper", Some(" 331")))
val validateTupleInvalidBoth3 = StreetSecondary.validate(("Upper!", Some(" 331")))
val validateTupleBoth2 = StreetSecondary.validate(("Ste", Some("100")))

To examine the contents of the cache after running the test cases, use this:

val cacheContents = StreetSecondary.cache.asMap

I reached this solution via a sequence of very carefully designed changes. I started with the simplest case class:

case class StreetSecondary (designator: String, value: Option[String]) //example A

And then I kept refactoring and enhancing the case class and its companion object until I got to the end solution (presented above, which is example J). I captured each progression in the evolution and turned it into an article (in a Google Drive document).

Here's the complete source code for the Eclipse ScalaIDE WorkSheet:

package org.public_domain

import scala.util.{Failure, Success, Try}
import java.util.concurrent._
import com.google.common.cache.CacheBuilder
import scalacache._
import scalacache.guava.{GuavaCache => GuavaCacheWrapper}
import scalacache.memoization._

object WorkSheetStreetSecondaryJ {
  println("required by ScalaIDE or a silent/unexplained execution failure occurs")
  object StreetSecondary extends ((String, Option[String]) => StreetSecondary) {
    type Parameters = (String, Option[String])

    val cache =
      CacheBuilder
        .newBuilder()
        .maximumSize(10000L)
        .expireAfterAccess(30, TimeUnit.SECONDS)
        .build[String, Object]
    implicit val scalaCache: ScalaCache = ScalaCache(GuavaCacheWrapper(cache))

    def apply(designator: String, value: Option[String] = None): StreetSecondary =
      validate(designator, value) match {
        case Some(runtimeExceptionsException) =>
          throw runtimeExceptionsException
        case None =>
          create(designator, value)
      }

    def apply(parameters: Parameters): StreetSecondary =
      StreetSecondary(parameters._1, parameters._2)

    def tryValidatedFactory(designator: String, value: Option[String] = None): Try[() => StreetSecondary] =
      validate(designator, value) match {
        case Some(runtimeExceptionsException) =>
          Failure(runtimeExceptionsException)
        case None =>
          Success(
            new (() => StreetSecondary) {
              def apply(): StreetSecondary =
                create(designator, value)
            }
          )
      }

    def tryValidatedFactory(parameters: Parameters): Try[() => StreetSecondary] =
      tryValidatedFactory(parameters._1, parameters._2)

    def validate(designator: String, value: Option[String] = None): Option[RuntimeExceptionsException] =
      RuntimeExceptionsException(clientProvidedValidateImpl(designator, value))

    def validate(parameters: Parameters): Option[RuntimeExceptionsException] =
      validate(parameters._1, parameters._2)

    def clientProvidedValidateImpl(designator: String, value: Option[String]): List[RuntimeException] = {
      def validateInRange(name: String, upperBound: Double, value: Double): Option[IllegalArgumentException] =
        if (!(-upperBound <= value))
          Some(new IllegalArgumentException(s"$name [$value] must be greater than or equal to ${-upperBound}"))
        else
          if (!(value < upperBound))
            Some(new IllegalArgumentException(s"$name [$value] must be less than $upperBound"))
          else
            None
      List(
          if (!designator.forall(char => ((char == '#') || char.isLetter)))
            Some(new IllegalStateException(s"designator [$designator] must only contain characters from [#,A-Za-z]"))
          else
            None
        , value.flatMap(
            valueGet =>
              if (valueGet.head == ' ')
                Some(new IllegalStateException(s"if defined, value [$valueGet] must not start with a space"))
              else
                None
          )
      ).flatten
    }

    private def create(designator: String, value: Option[String]): StreetSecondary = memoize {
      new StreetSecondary(designator, value) {
        private def readResolve(): Object =
          StreetSecondary(designator, value)

        def copy(designator: String = designator, value: Option[String] = value): StreetSecondary =
          StreetSecondary(designator, value)

        val tuple: StreetSecondary.Parameters =
          (designator, value)
      }
    }
  }
  abstract case class StreetSecondary private[StreetSecondary] (designator: String, value: Option[String]) { //example J
    def copy(designator: String = designator, value: Option[String] = value): StreetSecondary
    def tuple: StreetSecondary.Parameters
  }
  //
  val tryDesignator1 = Try(StreetSecondary("Upper"))
  val tryDesignator2 = Try(StreetSecondary("Upper", None))
  val tryBoth1 = Try(StreetSecondary("Upper", Some("331")))
  val tryInvalidDesignator = Try(StreetSecondary("Upper!", None))
  val tryInvalidBoth1 = Try(StreetSecondary("Upper!", Some("331")))
  val tryInvalidBoth2 = Try(StreetSecondary("Upper", Some(" 331")))
  val tryInvalidBoth3 = Try(StreetSecondary("Upper!", Some(" 331")))
  val tryBoth2 = Try(StreetSecondary("Ste", Some("100")))
  //
  //Note: these won't compile as the constructor is private - desired effect
  //val tryNewDesignator = Try(new StreetSecondary("Upper", None) { /* implementation intentionally left blank */})
  //val tryNewBoth = Try(new StreetSecondary("Upper", Some("331")) { /* implementation intentionally left blank */})
  //val tryNewInvalidDesignator = Try(new StreetSecondary("Upper!", None) { /* implementation intentionally left blank */})
  //val tryNewInvalidBoth1 = Try(new StreetSecondary("Upper!", Some("331")) { /* implementation intentionally left blank */})
  //val tryNewInvalidBoth2 = Try(new StreetSecondary("Upper!", Some(" 331")) { /* implementation intentionally left blank */})
  //val tryNewInvalidBOth3 = Try(new StreetSecondary("Upper", Some(" 331")) { /* implementation intentionally left blank */})
  //
  val tryCopyDesignator = Try(tryBoth2.get.copy(designator = "Upper"))
  val tryCopyValue = Try(tryBoth2.get.copy(value = Some("331")))
  val tryCopyBoth1 = Try(tryBoth2.get.copy(designator = "Upper", value = None))
  val tryCopyBoth2 = Try(tryBoth2.get.copy(designator = "Upper", value = Some("331")))
  val tryCopyInvalidDesignator = Try(tryBoth2.get.copy(designator = "Upper!", value = None))
  val tryCopyInvalidBoth1 = Try(tryBoth2.get.copy(designator = "Upper!", value = Some("331")))
  val tryCopyInvalidBoth2 = Try(tryBoth2.get.copy(designator = "Upper", value = Some(" 331")))
  val tryCopyInvalidBoth3 = Try(tryBoth2.get.copy(designator = "Upper!", value = Some(" 331")))
  //
  val validateDesignator1 = StreetSecondary.validate("Upper")
  val validateDesignator2 = StreetSecondary.validate("Upper", None)
  val validateBoth1 = StreetSecondary.validate("Upper", Some("331"))
  val validateInvalidDesignator = StreetSecondary.validate("Upper!", None)
  val validateInvalidBoth1 = StreetSecondary.validate("Upper!", Some("331"))
  val validateInvalidBoth2 = StreetSecondary.validate("Upper", Some(" 331"))
  val validateInvalidBoth3 = StreetSecondary.validate("Upper!", Some(" 331"))
  val validateBoth2 = StreetSecondary.validate("Ste", Some("100"))
  //
  val tryValidatedFactoryDesignator1 = StreetSecondary.tryValidatedFactory("Upper").map(_.apply())
  val tryValidatedFactoryDesignator2 = StreetSecondary.tryValidatedFactory("Upper", None).map(_.apply())
  val tryValidatedFactoryBoth1 = StreetSecondary.tryValidatedFactory("Upper", Some("331")).map(_.apply())
  val tryValidatedFactoryInvalidDesignator = StreetSecondary.tryValidatedFactory("Upper!", None).map(_.apply())
  val tryValidatedFactoryInvalidBoth1 = StreetSecondary.tryValidatedFactory("Upper!", Some("331")).map(_.apply())
  val tryValidatedFactoryInvalidBoth2 = StreetSecondary.tryValidatedFactory("Upper", Some(" 331")).map(_.apply())
  val tryValidatedFactoryInvalidBoth3 = StreetSecondary.tryValidatedFactory("Upper!", Some(" 331")).map(_.apply())
  val tryValidatedFactoryBoth2 = StreetSecondary.tryValidatedFactory("Ste", Some("100")).map(_.apply())
  //
  val tryTupledDesignator2 = Try(StreetSecondary.tupled(("Upper", None)))
  val tryTupledBoth1 = Try(StreetSecondary.tupled(("Upper", Some("331"))))
  val tryTupledInvalidDesignator = Try(StreetSecondary.tupled(("Upper!", None)))
  val tryTupledInvalidBoth1 = Try(StreetSecondary.tupled(("Upper!", Some("331"))))
  val tryTupledInvalidBoth2 = Try(StreetSecondary.tupled(("Upper", Some(" 331"))))
  val tryTupledInvalidBoth3 = Try(StreetSecondary.tupled(("Upper!", Some(" 331"))))
  val tryTupledBoth2 = Try(StreetSecondary.tupled(("Ste", Some("100"))))
  //
  val tryApplyTupleDesignator2 = Try(StreetSecondary(("Upper", None)))
  val tryApplyTupleBoth1 = Try(StreetSecondary(("Upper", Some("331"))))
  val tryApplyTupleInvalidDesignator = Try(StreetSecondary(("Upper!", None)))
  val tryApplyTupleInvalidBoth1 = Try(StreetSecondary(("Upper!", Some("331"))))
  val tryApplyTupleInvalidBoth2 = Try(StreetSecondary(("Upper", Some(" 331"))))
  val tryApplyTupleInvalidBoth3 = Try(StreetSecondary(("Upper!", Some(" 331"))))
  val tryApplyTupleBoth2 = Try(StreetSecondary(("Ste", Some("100"))))
  //
  val tryValidatedFactoryTupleDesignator2 = StreetSecondary.tryValidatedFactory(("Upper", None)).map(_.apply())
  val tryValidatedFactoryTupleBoth1 = StreetSecondary.tryValidatedFactory(("Upper", Some("331"))).map(_.apply())
  val tryValidatedFactoryTupleInvalidDesignator = StreetSecondary.tryValidatedFactory(("Upper!", None)).map(_.apply())
  val tryValidatedFactoryTupleInvalidBoth1 = StreetSecondary.tryValidatedFactory(("Upper!", Some("331"))).map(_.apply())
  val tryValidatedFactoryTupleInvalidBoth2 = StreetSecondary.tryValidatedFactory(("Upper", Some(" 331"))).map(_.apply())
  val tryValidatedFactoryTupleInvalidBoth3 = StreetSecondary.tryValidatedFactory(("Upper!", Some(" 331"))).map(_.apply())
  val tryValidatedFactoryTupleBoth2 = StreetSecondary.tryValidatedFactory(("Ste", Some("100"))).map(_.apply())
  //
  val validateTupleDesignator2 = StreetSecondary.validate(("Upper", None))
  val validateTupleBoth1 = StreetSecondary.validate(("Upper", Some("331")))
  val validateTupleInvalidDesignator = StreetSecondary.validate(("Upper!", None))
  val validateTupleInvalidBoth1 = StreetSecondary.validate(("Upper!", Some("331")))
  val validateTupleInvalidBoth2 = StreetSecondary.validate(("Upper", Some(" 331")))
  val validateTupleInvalidBoth3 = StreetSecondary.validate(("Upper!", Some(" 331")))
  val validateTupleBoth2 = StreetSecondary.validate(("Ste", Some("100")))
  //
  val cacheContents = StreetSecondary.cache.asMap
}
\$\endgroup\$
1
  • \$\begingroup\$ If you are going to downvote, at least take a second to say why so I have an opportunity to address your issue. Thank you. \$\endgroup\$ Aug 13, 2015 at 15:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.