Передача универсального сопутствующего объекта в супер конструктор
Я пытаюсь построить trait
и abstract class
подтипить сообщениями (в среде воспроизведения Akka), чтобы я мог легко преобразовать их в Json
,
Что сделали до сих пор:
abstract class OutputMessage(val companion: OutputMessageCompanion[OutputMessage]) {
def toJson: JsValue = Json.toJson(this)(companion.fmt)
}
trait OutputMessageCompanion[OT] {
implicit val fmt: OFormat[OT]
}
Проблема в том, когда я пытаюсь реализовать упомянутые интерфейсы следующим образом:
case class NotifyTableChange(tableStatus: BizTable) extends OutputMessage(NotifyTableChange)
object NotifyTableChange extends OutputMessageCompanion[NotifyTableChange] {
override implicit val fmt: OFormat[NotifyTableChange] = Json.format[NotifyTableChange]
}
Я получаю эту ошибку от Intellij:Type mismatch, expected: OutputMessageCompanion[OutputMessage], actual: NotifyTableChange.type
Я немного новичок в дженериках Scala - так что помощь с некоторыми объяснениями будет высоко ценится.
PS Я открыт для любых более общих решений, чем упомянутое. Цель заключается в том, чтобы при получении любого подтипа OutputMessage
- легко конвертировать в Json
,
2 ответа
Компилятор говорит, что ваш companion
определяется над OutputMessage
в качестве общего параметра, а не какой-то конкретный подтип. Чтобы обойти это, вы хотите использовать трюк, известный как F-связанный дженерик. Также мне не нравится идея сохранения этого объекта-компаньона как val
в каждом сообщении (ведь вы не хотите, чтобы оно сериализовалось?). Определяя это как def
ИМХО гораздо лучше компромисс. Код будет выглядеть следующим образом (компаньоны останутся прежними):
abstract class OutputMessage[M <: OutputMessage[M]]() {
self: M => // required to match Json.toJson signature
protected def companion: OutputMessageCompanion[M]
def toJson: JsValue = Json.toJson(this)(companion.fmt)
}
case class NotifyTableChange(tableStatus: BizTable) extends OutputMessage[NotifyTableChange] {
override protected def companion: OutputMessageCompanion[NotifyTableChange] = NotifyTableChange
}
Вы также можете увидеть стандартные коллекции Scala для реализации того же подхода.
Но если все, что вам нужно companion
для кодирования в формате JSON, вы можете избавиться от него следующим образом:
abstract class OutputMessage[M <: OutputMessage[M]]() {
self: M => // required to match Json.toJson signature
implicit protected def fmt: OFormat[M]
def toJson: JsValue = Json.toJson(this)
}
case class NotifyTableChange(tableStatus: BizTable) extends OutputMessage[NotifyTableChange] {
override implicit protected def fmt: OFormat[NotifyTableChange] = Json.format[NotifyTableChange]
}
Очевидно, что вы также хотите декодировать из JSON, вам все равно нужен сопутствующий объект.
Ответы на комментарии
- Обращение к компаньону через def - означает, что это "метод", определенный таким образом один раз для всех экземпляров подтипа (и не сериализуется)?
Все, что вы заявляете с val
получает поле, хранящееся в объекте (экземпляр класса). По умолчанию сериализаторы пытаются сериализовать все поля. Обычно есть какой-то способ сказать, что некоторые поля следует игнорировать (например, некоторые @IgnoreAnnotation
). Также это означает, что у вас будет еще один указатель / ссылка в каждом объекте, который использует память без веской причины, это может или не может быть проблемой для вас. Объявив это как def
получает метод, чтобы вы могли хранить только один объект в каком-то "статическом" месте, например, объект-компаньон, или каждый раз создавать его по требованию.
- Я немного новичок в Scala, и у меня появилась привычка помещать формат в объект-компаньон. Вы порекомендуете / обратитесь к какому-нибудь источнику, о том, как решить, где лучше всего разместить ваши методы?
Scala - необычный язык, и нет прямого отображения обложек всех вариантов использования object
концепция на других языках. Как правило, есть два основных способа использования object
:
Что-то, где вы бы использовали
static
в других языках, т. е. контейнер для статических методов, констант и статических переменных (хотя переменные не рекомендуется, особенно статические в Scala)Реализация шаблона синглтона.
- Под f-bound generic - подразумеваете ли вы нижнюю границу M, являющуюся OutputMessage[M] (кстати, почему нормально использовать M дважды в одном выражении?)
К сожалению, вики предоставляет только базовое описание. Вся идея F-ограниченного полиморфизма заключается в том, чтобы иметь возможность доступа к типу подкласса в типе базового класса каким-либо общим способом. Обычно A <: B
ограничение означает, что A
должен быть подтипом B
, Здесь с M <: OutputMessage[M]
, это означает, что M
должен быть подтипом OutputMessage[M]
что может быть легко удовлетворено только путем объявления дочернего класса (есть другие непростые способы удовлетворить это) как:
class Child extends OutputMessage[Child}
Такой трюк позволяет использовать M
как аргумент или тип результата в методах.
- Я немного озадачен собой немного...
Наконец self
бит это еще один трюк, который необходим, потому что F-ограниченного полиморфизма было недостаточно в данном конкретном случае. Обычно он используется с trait
когда черты используются в качестве дополнения. В таком случае вы можете захотеть ограничить, в какие классы можно смешивать черту. И в том же типе это позволяет вам использовать методы из этого базового типа в вашем миксине. trait
,
Я бы сказал, что конкретное использование в моем ответе немного необычно, но имеет тот же двойной эффект:
При компиляции
OutputMessage
компилятор может предположить, что тип также будет как-то типаM
(без разницыM
является)При компиляции любого подтипа компилятор гарантирует, что ограничение #1 выполнено. Например, это не позволит вам сделать
case class SomeChild(i: Int) extends OutputMessage[SomeChild]
// this will fail because passing SomeChild breaks the restriction of self:M
case class AnotherChild(i: Int) extends OutputMessage[SomeChild]
На самом деле, так как я должен был использовать self:M
в любом случае, вы можете удалить F-ограниченную часть здесь, живя просто
abstract class OutputMessage[M]() {
self: M =>
...
}
но я бы остался с этим, чтобы лучше передать смысл.
Как уже ответил SergGr, вам потребуется полиморфизм F-Bounded, чтобы решить эту проблему, как сейчас.
Тем не менее, для этих случаев, я считаю (обратите внимание, что это только мое мнение) лучше использовать классы типов.
В вашем случае вы хотите предоставить только toJson
метод к любому значению, пока у них есть экземпляр OFormat[T]
учебный класс.
Вы можете достичь этого с помощью этого (более простого ИМХО) кода.
object syntax {
object json {
implicit class JsonOps[T](val t: T) extends AnyVal {
def toJson(implicit: fmt: OFormat[T]): JsVal = Json.toJson(t)(fmt)
}
}
}
final case class NotifyTableChange(tableStatus: BizTable)
object NotifyTableChange {
implicit val fmt: OFormat[NotifyTableChange] = Json.format[NotifyTableChange]
}
import syntax.json._
val m = NotifyTableChange(tableStatus = ???)
val mJson = m.toJson // This works!
JsonOps
класс является неявным классом, который обеспечит toJson
метод для любого значения, для которого существует неявное OFormat
экземпляр в объеме.
И так как объект-компаньон NotifyTableChange
класс определяет такое неявное, оно всегда находится в области видимости - больше информации о том, где scala ищет следствия в этой ссылке.
Кроме того, учитывая, что это класс значений, этот метод расширения не требует создания экземпляров во время выполнения.
Здесь вы можете найти более подробное обсуждение F-Bounded и Typeclasses.