What am I doing wrong around adding an additional

2019-07-27 11:08发布

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.

2条回答
家丑人穷心不美
2楼-- · 2019-07-27 11:27

In this case I would use trait and companion object.

trait StreetSecondary1 {
  def designator: String
  def value: Option[String]
}

object StreetSecondary1 {
  // For `StreetSecondary1("str", Some("val"))`
  def apply(d: String, v: Option[String]): StreetSecondary1 =
    new StreetSecondary1 {
      val designator = d
      val value = v
    }

  // For `StreetSecondary1("raw")
  def apply(raw: String): StreetSecondary1 = {
    val (d, v) = parse(raw)
    apply(d, v)
  }

  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)
  }
}
查看更多
甜甜的少女心
3楼-- · 2019-07-27 11:50

You hit on a language restriction on auxiliary constructors

In Scala, every auxiliary constructor must invoke another constructor of the same class as its first action. In other words, the first statement in every auxiliary constructor in every Scala class will have the form this(. . . ). The invoked constructor is either the primary constructor (as in the Rational example), or another auxiliary constructor that comes textually before the calling constructor. The net effect of this rule is that every constructor invocation in Scala will end up eventually calling the primary constructor of the class. The primary constructor is thus the single point of entry of a class.

If you’re familiar with Java, you may wonder why Scala’s rules for constructors are a bit more restrictive than Java’s. In Java, a constructor must either invoke another constructor of the same class, or directly invoke a constructor of the superclass, as its first action. In a Scala class, only the primary constructor can invoke a superclass constructor. The increased restriction in Scala is really a design trade-off that needed to be paid in exchange for the greater conciseness and simplicity of Scala’s constructors compared to Java’s.

(Programming in Scala, chapter 6, section 7)

Therefore you cannot call this.tupled as the first statement of your auxiliary constructor.

Adding the constructor in the companion object is definitely the way to go in this case.


Not relevant to the question itself, but you can greatly simplify your parse method using span

private def parse(values: String): (String, Option[String]) = {
  val (designator, value) = values.span(_ != ' ')
  (designator, Option(value.trim).filter(_.nonEmpty))
} //                              ^^^^^^^^^^^^^^^^^^
  //                              returns None if the string is empty
查看更多
登录 后发表回答