Что я делаю неправильно, добавляя дополнительный конструктор класса case, который сначала преобразует его параметры?

Итак, у меня был очень простой класс case:

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

Это работало отлично. Тем не менее, у меня были места, где я разбирал одну строку в кортеж, который затем использовался для создания экземпляра этого класса case:

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)
  )
}

Итак, я хотел реорганизовать все разные места с помощью этого же кода синтаксического анализа в класс case, как это (но это не будет компилироваться):

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)
  }
}

Похоже, что возникает проблема с цыпленком/яйцом при добавлении конструктора класса case И с функцией, которая принимает параметр и преобразует их перед вызовом фактического конструктора. Я возился с этим (идя по многим касательным). Затем я прибег к попытке пути сопутствующего объекта:

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])

Что я делаю неправильно в StreetSecondary2? Есть ли способ заставить его работать? Разумеется, должен быть более простой способ, когда мне не нужно добавлять все шаблоны сопутствующих объектов, присутствующие в StreetSecondary3. Есть?

Спасибо за любые отзывы и рекомендации, которые вы можете мне дать.


ОБНОВЛЕНИЕ

Уф! Уже много уроков.

A) метод parse StreetSecondary2 не использует неявный контекст "this" в создаваемом экземпляре класса case (т.е. является статическим методом в терминах Java), поэтому он лучше перемещается в объект-компаньон.

B) К сожалению, при составлении явного сопутствующего объекта для класса case компилятор, предоставляющий "неявный сопутствующий объект", теряется. Метод пополнения (и другие, я угадываю - конечно, пожелал, чтобы был способ сохранить его и увеличить, в отличие от его удаления) содержались в компиляторе, снабженном "неявным сопутствующим объектом" и не предоставляемом в новом явном сопутствующем объекте. Это было исправлено добавлением "extends ((String, Option [String]) = > StreetSecondary) к явному сопутствующему объекту.

C) Здесь обновленное решение (которое также включает более краткий вариант функции синтаксического анализа с благодарностью Габриэле Петронелле):

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])

Это едва лучше с точки зрения шаблона, чем версия StreetSecondary3. Тем не менее, теперь это становится немного более ощутимым из-за того, что явный контекстный контекст стал явным.

Ответ 1

Вы нажимаете ограничение языка на вспомогательные конструкторы

В Scala каждый вспомогательный конструктор должен вызывать другой конструктор того же класса, что и его первое действие. Другими словами, первый оператор в каждом вспомогательном конструкторе в каждом классе Scala будет иметь вид this (...). Вызываемый конструктор является либо основным конструктором (как в примере Rational), либо другим вспомогательным конструктором, который идет текстовым образом перед вызывающим конструктором. Чистый эффект этого правила заключается в том, что каждый вызов конструктора в Scala в конечном итоге вызовет основной конструктор класса. Таким образом, основной конструктор является единственной точкой входа класса.

Если вы знакомы с Java, вы можете задаться вопросом, почему правила Scala s для конструкторов немного более строгие, чем Javas. В Java конструктор должен либо вызывать другой конструктор того же класса, либо непосредственно вызывать конструктор суперкласса в качестве своего первого действия. В классе Scala только основной конструктор может вызывать конструктор суперкласса. Увеличенное ограничение в Scala представляет собой конструктивный компромисс, который нужно было заплатить в обмен на большую лаконичность и простоту конструкторов Scala по сравнению с Javas.

(Программирование в Scala, глава 6, раздел 7)

Поэтому вы не можете вызывать this.tupled как первый оператор вашего вспомогательного конструктора.

Добавление конструктора в объект-компаньон - это, безусловно, способ пойти в этом случае.


Не относится к самому вопросу, но вы можете значительно упростить свой метод parse, используя 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

Ответ 2

В этом случае я бы использовал объект trait и companion.

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)
  }
}