Использование линз в обычных классах Scala

Большинство популярных библиотек JSON для Scala имеют возможность сериализовать и десериализовать классы case.

К сожалению, до выпуска Scala 2.11 существует ограничение на количество параметров, которые может иметь класс case (максимум 22). В качестве обходного пути для превышения этого ограничения можно вместо этого использовать обычные классы. (например: Как я могу десериализовать из JSON со Scala, используя классы *без регистра*?).

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

Есть ли способ сделать так, чтобы обычные классы вели себя как кейс-классы, чтобы, например, на них тоже работали линзы?


person Eduardo    schedule 22.06.2013    source источник
comment
Не совсем правильно говорить, что линзы не работают с обычными классами. Некоторые специальные библиотеки объективов (например, Rillit) могут предоставлять более удобный синтаксис для классов случаев, но Lens это очень простой интерфейс, и вы всегда можете определить свой собственный.   -  person Travis Brown    schedule 23.06.2013


Ответы (2)


Если вы все равно используете линзы, просто вложите классы case. У вас будет больше возможностей для повторного использования данных, и главная причина не вкладываться — избегать таких чудовищ, как

record.copy(person = record.person.copy(name = record.person.name.capitalize))

которые (в основном) решаются, если вы используете линзы. JSON может обрабатывать вложенные классы.

person Rex Kerr    schedule 23.06.2013

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

Ниже приведено доказательство работоспособности концепции (с использованием json4s и копии старой реализации объектива Scalaz, заимствованной из ответа Даниэля Собрала на более чистый способ обновления вложенных структур):

import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.native.JsonMethods._
import native.Serialization.write

class Parent(val name:String, val age:Int, val kids:List[Kid]){
  override def toString() = s"""$name is $age years old, her/his kids are ${kids.mkString(", ")}."""

  def copy(name:String = name, age:Int = age, kids:List[Kid] = kids) = 
    new Parent(name, age, kids)
}

class Kid(val name:String, val age:Int){
  override def toString() = s"""$name ($age)"""

  def copy(name:String = name, age:Int = age) = 
    new Kid(name, age)
}

object TestJson {
  implicit val formats = DefaultFormats

  val json = """{"name":"John", "age":41, "kids":[{"name":"Mary", "age":10}, {"name":"Tom", "age":7}]}"""

  def main(args: Array[String]): Unit = {
    val parentKidsLens = Lens(
      get = (_: Parent).kids, 
      set = (p: Parent, kids: List[Kid]) => p.copy(kids = kids))

    val firstKidLens = Lens(
      get = (_: List[Kid]).head,
      set = (kds: List[Kid], kid: Kid) => kid :: kds.tail)

    val kidAgeLens = Lens(
      get = (_: Kid).age,
      set = (k: Kid, age: Int) => k.copy(age = age))

    val parentFirstKidAgeLens = parentKidsLens andThen firstKidLens andThen kidAgeLens


    println( parentFirstKidAgeLens.mod(parse(json).extract[Parent], age => age + 1) )    
  }
}

case class Lens[A,B](get: A => B, set: (A,B) => A) extends Function1[A,B] with Immutable {
  def apply(whole: A): B   = get(whole)
  def updated(whole: A, part: B): A = set(whole, part)
  def mod(a: A, f: B => B) = set(a, f(this(a)))
  def compose[C](that: Lens[C,A]) = Lens[C,B](
    c => this(that(c)),
    (c, b) => that.mod(c, set(_, b))
  )
  def andThen[C](that: Lens[B,C]) = that compose this
}
person Eduardo    schedule 22.06.2013