Передача универсального сопутствующего объекта в супер конструктор

Я пытаюсь построить 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, вам все равно нужен сопутствующий объект.


Ответы на комментарии

  1. Обращение к компаньону через def - означает, что это "метод", определенный таким образом один раз для всех экземпляров подтипа (и не сериализуется)?

Все, что вы заявляете с val получает поле, хранящееся в объекте (экземпляр класса). По умолчанию сериализаторы пытаются сериализовать все поля. Обычно есть какой-то способ сказать, что некоторые поля следует игнорировать (например, некоторые @IgnoreAnnotation). Также это означает, что у вас будет еще один указатель / ссылка в каждом объекте, который использует память без веской причины, это может или не может быть проблемой для вас. Объявив это как def получает метод, чтобы вы могли хранить только один объект в каком-то "статическом" месте, например, объект-компаньон, или каждый раз создавать его по требованию.

  1. Я немного новичок в Scala, и у меня появилась привычка помещать формат в объект-компаньон. Вы порекомендуете / обратитесь к какому-нибудь источнику, о том, как решить, где лучше всего разместить ваши методы?

Scala - необычный язык, и нет прямого отображения обложек всех вариантов использования object концепция на других языках. Как правило, есть два основных способа использования object:

  1. Что-то, где вы бы использовали static в других языках, т. е. контейнер для статических методов, констант и статических переменных (хотя переменные не рекомендуется, особенно статические в Scala)

  2. Реализация шаблона синглтона.

  1. Под f-bound generic - подразумеваете ли вы нижнюю границу M, являющуюся OutputMessage[M] (кстати, почему нормально использовать M дважды в одном выражении?)

К сожалению, вики предоставляет только базовое описание. Вся идея F-ограниченного полиморфизма заключается в том, чтобы иметь возможность доступа к типу подкласса в типе базового класса каким-либо общим способом. Обычно A <: B ограничение означает, что A должен быть подтипом B, Здесь с M <: OutputMessage[M], это означает, что M должен быть подтипом OutputMessage[M] что может быть легко удовлетворено только путем объявления дочернего класса (есть другие непростые способы удовлетворить это) как:

class Child extends OutputMessage[Child}

Такой трюк позволяет использовать M как аргумент или тип результата в методах.

  1. Я немного озадачен собой немного...

Наконец self бит это еще один трюк, который необходим, потому что F-ограниченного полиморфизма было недостаточно в данном конкретном случае. Обычно он используется с trait когда черты используются в качестве дополнения. В таком случае вы можете захотеть ограничить, в какие классы можно смешивать черту. И в том же типе это позволяет вам использовать методы из этого базового типа в вашем миксине. trait,

Я бы сказал, что конкретное использование в моем ответе немного необычно, но имеет тот же двойной эффект:

  1. При компиляции OutputMessage компилятор может предположить, что тип также будет как-то типа M (без разницы M является)

  2. При компиляции любого подтипа компилятор гарантирует, что ограничение #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.

Другие вопросы по тегам