Как выразить этот тип в Scala? Экзистенциальный с ограничением класса типа (т. е. неявным)?

Я использую библиотеку JSON платформы Play, которая использует класс типов для реализации Json.toJsonфункция. (Я могу решить использовать другой метод с менее статической типизацией, такой как отражение, но сейчас я хочу использовать эту библиотеку, потому что она помогает мне изучить систему типов Scala.)

У меня есть куча простых классов case, которые нужно передать в toJson, поэтому я должен реализовать неявный объект Writes[T] для каждого из них. Первый разрез может выглядеть так для каждого из классов.

// An example class
case class Foo(title: String, lines: List[String])

// Make 'Foo' a member of the 'Writes' typeclass
implicit object FooWrites extends Writes[Foo] {
  def writes(f: Foo) : JsValue = {
    val fields = Seq("title" -> toJson(f.title), 
                     "lines" -> toJson(f.lines))                        
    JsObject(fields)
  }
}  

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

def makeSimpleWrites[C](fields: (String, C => T??)*) : Writes[C] = {
  new Writes[C] {
    def writes(c: C) : JsValue = {
      val jsFields = fields map { case (name, get) => (name, toJson(get(c)))}
      JsObject(jsFields)
    }
  }
}

implicit val fooWrites : Writes[Foo] = 
    makeSimpleWrites[Foo]("title" -> {_.title}, "lines" -> {_.lines})                                 
implicit val otherWrites ...

Проблема в типе T, который я хочу передать makeSimpleWrites. Это не может быть параметр нормального типа, потому что T отличается для каждого элемента в fields. Это экзистенциальный тип? Мне еще предстоит использовать один из них. Заморачиваюсь над синтаксисом...

def makeSimpleWrites[C](fields: (String, C=>T forSome { type T; implicit Writes[T] })*) 

Возможно ли это в Scala? Если да, то каков синтаксис?


person Rob N    schedule 13.12.2012    source источник
comment
Учитывая, что ОП явно упоминает классы типов, я думаю, он знает, что это такое, по крайней мере, поверхностно, верно? Или, может быть, вы можете указать на конкретную деталь классов типов, которые могли бы помочь ему в этом случае?   -  person Régis Jean-Gilles    schedule 13.12.2012
comment
Правильно, я думаю, что понимаю шаблон класса типов.   -  person Rob N    schedule 13.12.2012
comment
Действительно, извините, я слишком быстро прочитал ваш вопрос ... Единственное ограничение для типа T - это то, что он должен иметь неявный тип Writes[T] ? Вы можете использовать def makeSimpleWrites[C, T : Writes](fields: (String, C=>T)*)   -  person Lomig Mégard    schedule 13.12.2012


Ответы (3)


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

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

class WriteSFieldAccessor[C,T] private ( val title: String, val accessor: C => Any )( implicit val writes: Writes[T] )

implicit def toWriteSFieldAccessor[C,T:Writes]( titleAndAccessor: (String, C => T) ): WriteSFieldAccessor = {
  new WriteSFieldAccessor[C,T]( titleAndAccessor._1, titleAndAccessor._2 )
}
def makeSimpleWrites[C](fields: WriteSFieldAccessor[C,_]*) : Writes[C] = {
  new Writes[C] {
    def writes(c: C) : JsValue = {
      val jsFields = fields map { f: WriteSFieldAccessor => 
        val jsField = toJson[Any](f.accessor(c))(f.writes.asInstanceOf[Writes[Any]])
        (f.title, jsField)
      }
      JsObject(jsFields)
    }
  }
}

// Each pair below is implicitly converted to a WriteSFieldAccessor  instance, capturing the required information and passing it to makeSimpleWrites
implicit val fooWrites : Writes[Foo] = makeSimpleWrites[Foo]("title" -> {_.title}, "lines" -> {_.lines}) 

Самое интересное toJson[Any](f.accessor(c))(f.writes..asInstanceOf[Writes[Any]]). Вы просто передаете Any как статический тип, но явно передаете (обычно неявный) экземпляр Writes.

person Régis Jean-Gilles    schedule 13.12.2012
comment
Очень хороший ответ! Я забыл, что здесь нам понадобится один параметр типа для каждого поля... - person Lomig Mégard; 13.12.2012
comment
Ага, кажется, я понимаю, почему мне нужно упаковать неявные записи, как это сделали вы. Хотя теперь у меня возникли проблемы с компиляцией. Я добавил (f.writes.asInstanceOf[Any]), но теперь ему не нравятся анонимные функции в последней строке. missing parameter type for expanded function ((x$1) => x$1.title). Я сообщу больше позже. - person Rob N; 13.12.2012
comment
Скорее это будет f.writes.asInstanceOf[Writes[Any]]. Я обновлю свой ответ. - person Régis Jean-Gilles; 13.12.2012
comment
Похоже, мне нужно написать "title" -> {s:Section => s.title} вместо "title" -> {_.title}, чтобы он скомпилировался. Ничего страшного, но есть ли у вас еще какие-нибудь хитрости, чтобы это исправить? - person Rob N; 14.12.2012
comment
Посмотрите на мой другой ответ. - person Régis Jean-Gilles; 15.12.2012

При попытке устранить ограничение, согласно которому в моем первом решении нужно писать "title" -> {s:Section => s.title} вместо "title" -> {_.title}, я немного повозился с ним, только чтобы все время работать с ограничением вывода scala. Поэтому я решил попробовать решить эту проблему с другой стороны и нашел совершенно другое решение. Это в основном квази-DSL:

class ExpandableWrites[C]( val fields: Vector[(String, C => Any, Writes[_])] ) extends Writes[C] {
  def and[T:Writes](fieldAccessor: C => T)(fieldName: String): ExpandableWrites[C] = {
    new ExpandableWrites( fields :+ (fieldName, fieldAccessor, implicitly[Writes[T]]) )
  }
  def writes(c: C) : JsValue = {
    val jsFields = fields map { case (name, get, writes) => (name, toJson[Any](get(c))(writes.asInstanceOf[Writes[Any]]) )}
    JsObject(jsFields)
  }
}

class UnaryExpandableWritesFactory[C] {
  def using[T:Writes](fieldAccessor: C => T)(fieldName: String): ExpandableWrites[C] = {
    new ExpandableWrites[C]( Vector( (fieldName, fieldAccessor, implicitly[Writes[T]] ) ) )
  }
}

def makeSimpleWritesFor[C] = new UnaryExpandableWritesFactory[C]

implicit val fooWrites : Writes[Foo] = 
  makeSimpleWritesFor[Foo].using(_.title)("title") .and (_.lines)("lines") .and (_.date)("date")

Идея состоит в том, что вы шаг за шагом создаете экземпляр Writes и добавляете в него новые поля одно за другим. Единственное раздражение заключается в том, что вам нужен разделитель .and, включая точку. Без точки (то есть с использованием инфиксной нотации) компилятор, кажется, снова запутался и жалуется, если мы просто делаем (_.title) вместо (s:Section => s.title).

person Régis Jean-Gilles    schedule 14.12.2012

По крайней мере, на 25 января 2015 года play-json уже имеет встроенный способ делать то, что вы хотите:

import play.api.libs.json._
import play.api.libs.functional.syntax._

sealed case class Foo(title: String, lines: List[String])  // the `sealed` bit is not relevant but I always seal my ADTs

implicit val fooWrites = (
  (__ \ "title").write[String] ~
  (__ \ "lines").write[List[String]]
)(unlift(Foo.unapply))

на самом деле это работает и с Reads[T]

implicit val fooReads = (
  (__ \ "title").read[String] ~
  (-- \ "lines").read[List[String]]
)(Foo.apply _)

и Format[T]:

implicit val fooFormat = (
  (__ \ "title").format[String] ~
  (-- \ "lines").format[List[String]]
)(Foo.apply _, unlift(Foo.unapply))

вы также можете применять преобразования, например:

implicit val fooReads = (
  (__ \ "title").read[String].map(_.toLowerCase) ~
  (-- \ "lines").read[List[String]].map(_.filter(_.nonEmpty))
)(Foo.apply _)

или даже двусторонние преобразования:

implicit val fooFormat = (
  (__ \ "title").format[String].inmap(_.toLowerCase, _.toUpperCase) ~
  (-- \ "lines").format[List[String]]
)(Foo.apply _, unlift(Foo.unapply))
person Erik Kaplun    schedule 25.01.2015