Передача универсального сопутствующего объекта в супер конструктор - PullRequest
0 голосов
/ 21 января 2019

Я пытаюсь создать 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, так что помощь с некоторыми объяснениями была бы очень признательна.

P.S. Я открыт для любых более общих решений, чем упомянутое. Цель состоит в том, чтобы при получении любого подтипа OutputMessage - легко преобразовать его в Json.

Ответы [ 2 ]

0 голосов
/ 21 января 2019

Как уже ответил SergGr, вам понадобится полиморфизм F-Bound для решения этой проблемы, как сейчас.
Однако для этих случаев я считаю (обратите внимание, это только мое мнение) *Вместо 1003 * лучше использовать Классы типов .

В вашем случае вы хотите предоставить метод 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 ищет следствия в этой ссылке .
Кроме того, учитывая, что это Класс значений , этот метод расширения не требует создания экземпляров в runtime .

Здесь , вы можете найти более подробное обсуждение о F-Bounded против Типы классов .

0 голосов
/ 21 января 2019

Компилятор говорит, что ваш companion определен над OutputMessage как общий параметр, а не какой-то конкретный подтип. Чтобы обойти это, вы хотите использовать трюк, известный как F-bound generic . Также мне не нравится идея сохранения этого сопутствующего объекта как 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. Реализация шаблона Singleton.

  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 =>
     ...
}

но я бы остался с этим, чтобы лучше передать смысл.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...