Как переопределить применение в случае класса companion

84

Итак, вот ситуация. Я хочу определить класс case следующим образом:

case class A(val s: String)

и я хочу определить объект, чтобы при создании экземпляров класса значение 's' всегда было в верхнем регистре, например:

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

Однако это не работает, поскольку Scala жалуется, что метод apply (s: String) определен дважды. Я понимаю, что синтаксис класса case автоматически определит его для меня, но разве нет другого способа добиться этого? Я хотел бы придерживаться класса case, так как хочу использовать его для сопоставления с образцом.

Джон С
источник
3
Возможно, измените заголовок на «Как переопределить применение в классе-компаньоне»
ziggystar
1
Не используйте сахар, если он не дает того, что вы хотите ...
Рафаэль
7
@Raphael Что делать, если вам нужен коричневый сахар, то есть нам нужен сахар с некоторыми специальными атрибутами ... У меня точно такой же запрос, что и OP: классы case v полезны, но это достаточно распространенный вариант использования, чтобы захотеть украсить сопутствующий объект с помощью дополнительная заявка.
StephenBoesch
К вашему сведению: Это исправлено в scala 2.12+. Определение в сопутствующем методе применения, которое в противном случае может противоречить, предотвращает создание метода применения по умолчанию.
stewSquared

Ответы:

90

Причина конфликта в том, что класс case предоставляет точно такой же метод apply () (та же подпись).

Прежде всего, я хотел бы предложить вам использовать require:

case class A(s: String) {
  require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s)
}

Это вызовет исключение, если пользователь попытается создать экземпляр, в котором s включает символы нижнего регистра. Это хорошее использование классов case, поскольку то, что вы помещаете в конструктор, также вы получаете, когда используете сопоставление с образцом ( match).

Если это не то, что вы хотите, я бы создал конструктор privateи заставил пользователей использовать только метод apply:

class A private (val s: String) {
}

object A {
  def apply(s: String): A = new A(s.toUpperCase)
}

Как видите, A больше не a case class. Я не уверен, что классы case с неизменяемыми полями предназначены для модификации входящих значений, поскольку имя «case class» подразумевает, что должна быть возможность извлекать (немодифицированные) аргументы конструктора с помощью match.

Olle Kullberg
источник
5
toCharArrayВызов не нужно, можно было бы написать s.exists(_.isLower).
Фрэнк С. Томас,
4
Кстати, я думаю, s.forall(_.isUpper)это легче понять, чем !s.exists(_.isLower).
Фрэнк С. Томас,
благодаря! Это определенно работает для моих нужд. @Frank, я согласен, s.forall(_isupper)это легче читать. Я буду использовать это в сочетании с предложением @olle.
John S
4
+1 для "name" case class "подразумевает, что должна быть возможность извлекать (немодифицированные) аргументы конструктора, используя match".
Eugen Labun
2
@ollekullberg Вам не нужно отказываться от использования класса case (и терять все дополнительные преимущества, которые класс case предоставляет по умолчанию), чтобы достичь желаемого эффекта OP. Если вы сделаете две модификации, вы можете получить свой класс кейса и съесть его! A) пометьте класс case как абстрактный и B) пометьте конструктор класса case как частный [A] (в отличие от просто частного). Есть еще несколько более тонких проблем, связанных с расширением классов case с помощью этого метода. Пожалуйста, смотрите ответ, который я опубликовал, для более подробной информации: stackoverflow.com/a/25538287/501113
chaotic3quilibrium
28

ОБНОВЛЕНИЕ 2016/02/25:
хотя ответ, который я написал ниже, остается достаточным, стоит также сослаться на другой связанный ответ на этот вопрос, касающийся сопутствующего объекта класса case. А именно, как точно воспроизвести сгенерированный компилятором неявный объект-компаньон, который возникает, когда определяется только сам класс case. Для меня это оказалось нелогичным.


Резюме:
Вы можете довольно просто изменить значение параметра класса case до того, как он будет сохранен в классе case, пока он все еще остается действительным (отмеченным) ADT (абстрактный тип данных). Хотя решение было относительно простым, раскрыть детали было немного сложнее.

Подробности:
если вы хотите гарантировать, что только допустимые экземпляры вашего класса case могут когда-либо быть созданы, что является важным предположением, лежащим в основе ADT (абстрактного типа данных), вы должны сделать ряд вещей.

Например, сгенерированный компилятором copyметод предоставляется по умолчанию для класса case. Таким образом, даже если вы очень внимательно следите за тем, чтобы с помощью явного applyметода сопутствующего объекта были созданы только экземпляры, который гарантировал, что они могут содержать только значения в верхнем регистре, следующий код создаст экземпляр класса case со значением нижнего регистра:

val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"

Кроме того, классы case реализуют java.io.Serializable. Это означает, что ваша осторожная стратегия использования экземпляров только в верхнем регистре может быть нарушена с помощью простого текстового редактора и десериализации.

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

  1. Для вашего явного объекта-компаньона:
    1. Создайте его, используя то же имя, что и ваш класс case
      • У него есть доступ к частным частям класса case
    2. Создайте applyметод с точно такой же сигнатурой, что и основной конструктор для вашего класса дела.
      • Это будет успешно скомпилировано после завершения шага 2.1.
    3. Предоставьте реализацию, получающую экземпляр класса case с помощью newоператора и предоставляющую пустую реализацию{}
      • Теперь это создаст экземпляр класса case строго на ваших условиях.
      • {}Должна быть предоставлена пустая реализация , потому что класс case объявлен abstract(см. Шаг 2.1)
  2. Для вашего класса дела:
    1. Объявить это abstract
      • Запрещает компилятору Scala генерировать applyметод в сопутствующем объекте, который вызывал ошибку компиляции «метод определен дважды ...» (шаг 1.2 выше)
    2. Отметьте основной конструктор как private[A]
      • Основной конструктор теперь доступен только самому классу case и его сопутствующему объекту (тот, который мы определили выше на шаге 1.1)
    3. Создать readResolveметод
      1. Предоставьте реализацию с помощью метода apply (шаг 1.2 выше)
    4. Создать copyметод
      1. Определите его так, чтобы он имел точно такую ​​же сигнатуру, что и основной конструктор класса case
      2. Для каждого параметра, добавить значение по умолчанию с тем же именем параметра (например: s: String = s)
      3. Предоставьте реализацию с помощью метода apply (шаг 1.2 ниже)

Вот ваш код, измененный указанными выше действиями:

object A {
  def apply(s: String, i: Int): A =
    new A(s.toUpperCase, i) {} //abstract class implementation intentionally empty
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

И вот ваш код после реализации требования (предложенного в ответе @ollekullberg), а также определения идеального места для размещения любого вида кеширования:

object A {
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i) {} //abstract class implementation intentionally empty
  }
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

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

object A {
  private[A] abstract case class AImpl private[A] (s: String, i: Int)
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i)
  }
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

Хотя это прямо отвечает на ваш вопрос, есть еще больше способов расширить этот путь вокруг классов случаев, помимо кеширования экземпляров. Для нужд моего собственного проекта я создал еще более обширное решение, которое я задокументировал на CodeReview (родственный сайт StackOverflow). Если вы в конечном итоге изучите его, воспользуетесь моим решением, оставьте мне отзыв, предложения или вопросы, и в пределах разумного я сделаю все возможное, чтобы ответить в течение дня.

хаотическое3равновесие
источник
Я только что опубликовал более новое расширенное решение, чтобы сделать его более идиоматическим для Scala и включить использование ScalaCache для простого кэширования экземпляров класса case (не разрешалось редактировать существующий ответ в соответствии с мета-правилами): codereview.stackexchange.com/a/98367/4758
chaotic3quilibrium
спасибо за это подробное объяснение. Но я изо всех сил пытаюсь понять, почему требуется реализация readResolve. Потому что компиляция также работает без реализации readResolve.
mogli
опубликовал отдельный вопрос: stackoverflow.com/questions/32236594/…
mogli
12

Я не знаю, как переопределить applyметод в сопутствующем объекте (если это вообще возможно), но вы также можете использовать специальный тип для строк в верхнем регистре:

class UpperCaseString(s: String) extends Proxy {
  val self: String = s.toUpperCase
}

implicit def stringToUpperCaseString(s: String) = new UpperCaseString(s)
implicit def upperCaseStringToString(s: UpperCaseString) = s.self

case class A(val s: UpperCaseString)

println(A("hello"))

Приведенный выше код выводит:

A(HELLO)

Вы также должны взглянуть на этот вопрос и ответить на него: Scala: можно ли переопределить конструктор класса case по умолчанию?

Фрэнк С. Томас
источник
Спасибо за это - я думал в том же духе, но не знал об этом Proxy! Хотя может быть лучше s.toUpperCase один раз .
Бен Джексон
@Ben Я не вижу, куда звонят toUpperCaseбольше одного раза.
Фрэнк С. Томас
ты совершенно прав val self, нет def self. У меня только что в голове С ++.
Бен Джексон
6

Для людей, читающих это после апреля 2017 года: Начиная с Scala 2.12.2+, Scala по умолчанию позволяет переопределить применение и отмену . Вы можете получить такое поведение, также предоставив -Xsource:2.12возможность компилятору Scala 2.11.11+.

Мехмет Эмре
источник
1
Что это значит? Как я могу применить эти знания к решению? Вы можете привести пример?
k0pernikus
Обратите внимание , что исключить его не используются для случая классов соответствия шаблона, что делает его довольно временную отмену бесполезно (если вы заявление вы увидите , что она не используется). -Xprintmatch
J Cracknell
5

Он работает с переменными var:

case class A(var s: String) {
   // Conversion
   s = s.toUpperCase
}

Эта практика, по-видимому, поощряется в случае классов вместо определения другого конструктора. Глянь сюда. . При копировании объекта вы также сохраняете те же изменения.

Микаэль Майер
источник
4

Другая идея при сохранении класса case и без неявных defs или другого конструктора - сделать сигнатуру applyнемного другой, но с точки зрения пользователя такой же. Где-то я видел неявный трюк, но не могу вспомнить / найти, какой это был неявный аргумент, поэтому выбрал Booleanздесь. Если кто-то может мне помочь и довести дело до конца ...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)
Петер Шмитц
источник
На сайтах звонков будет выдана ошибка компиляции (неоднозначная ссылка на перегруженное определение). Это работает, только если типы scala различны, но остаются одинаковыми после стирания, например, чтобы иметь две разные функции для List [Int] и List [String].
Mikaël Mayer
Я не мог заставить этот способ решения работать (с 2.11). Я наконец понял, почему он не может предоставить свой собственный метод apply для явного сопутствующего объекта. Я подробно изложил это в только что опубликованном ответе: stackoverflow.com/a/25538287/501113
chaotic3quilibrium
3

Я столкнулся с той же проблемой, и это решение мне подходит:

sealed trait A {
  def s:String
}

object A {
  private case class AImpl(s:String)
  def apply(s:String):A = AImpl(s.toUpperCase)
}

И, если нужен какой-либо метод, просто определите его в трейте и переопределите в классе case.

Пере Рамирес
источник
0

Если вы застряли в более старой версии scala, где вы не можете переопределить по умолчанию или не хотите добавлять флаг компилятора, как показывает @ mehmet-emre, и вам нужен класс case, вы можете сделать следующее:

case class A(private val _s: String) {
  val s = _s.toUpperCase
}
критиум
источник
0

Начиная с 2020 года на Scala 2.13 описанный выше сценарий переопределения метода apply класса case с той же подписью работает полностью нормально.

case class A(val s: String)

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

приведенный выше фрагмент компилируется и отлично работает в Scala 2.13 как в режиме REPL, так и в режиме без REPL.

Гана
источник
-2

Я думаю, это работает именно так, как вы этого хотите. Вот моя сессия REPL:

scala> case class A(val s: String)
defined class A

scala> object A {
     | def apply(s: String) = new A(s.toUpperCase)
     | }
defined module A

scala> A("hello")
res0: A = A(HELLO)

Используется Scala 2.8.1.final

Mattrjacobs
источник
3
Здесь не работает, если я помещаю код в файл и пытаюсь его скомпилировать.
Фрэнк С. Томас
Я полагаю, что предлагал нечто подобное в более раннем ответе, и кто-то сказал, что это работает только в ответе из-за того, как работает ответ.
Бен Джексон
5
REPL, по сути, создает новую область видимости с каждой строкой внутри предыдущей. Вот почему некоторые вещи не работают должным образом при вставке из REPL в ваш код. Так что всегда проверяйте и то, и другое.
gregturn
1
Правильный способ проверить приведенный выше код (который не работает) - использовать: paste в REPL, чтобы убедиться, что и регистр, и объект определены вместе.
StephenBoesch