Использование линз на регулярных классах Scala
Самые популярные библиотеки JSON для Scala имеют возможность сериализации и десериализации в случае классов.
К сожалению, до выхода Scala 2.11 существует ограничение на количество параметров, которое может иметь класс case (максимум 22). Чтобы обойти это ограничение, можно использовать обычные классы. (например: Как я могу десериализовать из JSON с помощью Scala, используя * не-case* классы?).
Однако это теряет преимущества кейс-классов. Например, не существует автоматически сгенерированного конструктора копирования, и линзы не работают с обычными классами, поэтому манипулирование структурой становится очень громоздким (если только каждое поле в классе не становится var
отказавшись от преимуществ неизменности).
Есть ли способ заставить обычные классы вести себя больше как классы case, чтобы, например, линзы также работали с ними?
2 ответа
В любом случае, если вы используете линзы, просто вложите свои кейсы. У вас будет больше возможностей для повторного использования данных, и главная причина не вкладывать их в это, чтобы избежать таких чудовищ, как
record.copy(person = record.person.copy(name = record.person.name.capitalize))
которые (в основном) решаются, если вы используете линзы. JSON может обрабатывать вложенные классы.
Похоже, что, определяя функцию копирования (к сожалению, вручную), обычные классы могут работать с линзами, как упомянул Трэвис в своем комментарии к вопросу выше.
Ниже приведено доказательство работоспособности концепции (с использованием 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
}