So, I had a very simple case class:
case class StreetSecondary1(designator: String, value: Option[String])
This was working just fine. However, I kept having places where I was parsing a single string into a tuple which was then used to build an instance of this case class:
def parse1(values: String): StreetSecondary1 = {
val index = values.indexOf(" ")
StreetSecondary1.tupled(
if (index > -1)
//clip off string prior to space as designator and optionally use string after space as value
(values.take(index), if (values.size > index + 1) Some(values.drop(index + 1)) else None)
else
//no space, so only designator could have been provided
(values, None)
)
}
So, I wanted to refactor all the different places with this same parsing code into the case class like this (but this won't compile):
case class StreetSecondary2(designator: String, value: Option[String]) {
def this(values: String) = this.tupled(parse(values))
private def parse(values: String): (String, Option[String]) = {
val index = values.indexOf(" ")
if (index > -1)
//clip off string prior to space as designator and optionally use string after space as value
(values.take(index), if (values.size > index + 1) Some(values.drop(index + 1)) else None)
else
//no space, so only designator could have been provided
(values, None)
}
}
It appears there is some chicken/egg problem around adding a case class constructor AND having a function that takes the parameter(s) and transforms them prior to calling the actual constructor. I have fiddled with this (going on many tangents). I then resorted to trying the companion object pathway:
object StreetSecondary3 {
private def parse(values: String): (String, Option[String]) = {
val index = values.indexOf(" ")
if (index > -1)
//clip off string prior to space as designator and optionally use string after space as value
(values.take(index), if (values.size > index + 1) Some(values.drop(index + 1)) else None)
else
//no space, so only designator could have been provided
(values, None)
}
def apply(values: String): StreetSecondary3 = {
val tuple: (String, Option[String]) = parse(values)
StreetSecondary3(tuple._1, tuple._2) //Why doesn't .tupled method work here?
}
}
case class StreetSecondary3(designator: String, value: Option[String])
What am I doing wrong in StreetSecondary2? Is there some way to get it to work? Surely there has to be a better simpler way where I am not required to add all the companion object boilerplate present in StreetSecondary3. Is there?
Thank you for any feedback and guidance you can give me around this.
UPDATE
Whew! Lots of lessons learned already.
A) the StreetSecondary2 parse method does not use the "this" implicit context in the case class instance being constructed (i.e. it is a static method in Java terms), so it works better moved to the companion object.
B) Unfortunately when composing an explicit companion object for a case class, the compiler provided "implicit companion object" is lost. The tupled method (and others, I am guessing - sure wish there was a way to keep it and augment as opposed to blowing it away) were contained in the compiler provided "implicit companion object" and not provided in the new explicit companion object. This was fixed by adding "extends ((String, Option[String]) => StreetSecondary)" to the explicit companion object.
C) Here's an updated solution (which also incorporates a more terse version of the parse function with a nod of thanks to Gabriele Petronella):
object StreetSecondary4 extends ((String, Option[String]) => StreetSecondary4) {
private def parseToTuple(values: String): (String, Option[String]) = {
val (designator, value) = values.span(_ != ' ')
(designator, Option(value.trim).filter(_.nonEmpty))
}
def apply(values: String): StreetSecondary4 =
StreetSecondary4.tupled(parseToTuple(values))
}
case class StreetSecondary4(designator: String, value: Option[String])
This is barely better in terms of boilerplate than the StreetSecondary3 version. However, it now makes quite a bit more sense due to so much implicit context having now been made explicit.