关于json:如何从scala案例类中装饰不可变对象图

How to decorate an immutable object graph from scala case classes

我正在阅读结构化JSON,使用PlayFrameworks的JSON reads构建一个包含case类的对象图。

一个例子:

1
2
3
4
5
6
7
8
9
10
11
case class Foo (
                       id: Int,
                       bar_id: Int,
                       baz_id: Int,
                       x: Int,
                       y: String
                       )
{
  var bar: Bar = null
  var baz: Baz = null
}

比赛场地建成后,我必须晚些时候回来,通过设置酒吧和酒吧来装饰它。这些定义在其他JSON文件中,只有在所有解析完成时才知道。但这意味着foo不能是不变的。

在scala中,什么是"正确"的方法来生成一个不变的对象,然后是它的装饰版本,而不重复foo的每个字段多次、多次?

我知道有几种感觉不对劲的方法:

  • 将"bar:option[bar]"和"baz:option[baz]"作为case类参数,然后我可以使用"copy"使foo类的新版本设置为某个值;但是我必须在每次访问它们时检查它们-效率低下、不安全、无法生成保证具有正确结构的装饰foo
  • 创建第二个case类,它是第一个结构中所有结构的复制粘贴,但添加了两个额外的修饰参数-但这意味着在定义中回显整个参数列表,并在创建它的实例时再次
  • case类继承显然是有争议的,而且在任何情况下,似乎都要求我在子类构造函数中重复每个参数?
  • 生成一个非case超类,列出通用case类参数。然后在case类中扩展它。但这似乎仍然需要重复子类构造函数中的每个参数。
  • 我看到有人在博客中谈论这个问题,并在运行时使用反射来填充其装饰副本的基本属性——这可以避免回声,但现在您没有类型安全性,将属性名指定为字符串、开销等。

当然,scala必须有一种方法,让人们不用手工复制简单对象中的每一个部分,就可以组合出更复杂的不变对象?


结合Option和类型参数,可以标记case类,并静态跟踪处理的字段是否为空:

1
2
3
4
5
6
7
8
9
10
11
12
13
import scala.language.higherKinds

object Acme {
  case class Foo[T[X] <: Option[X] forSome { type X }](a: Int,
                                                       b: String,
                                                       c: T[Boolean],
                                                       d: T[Double])

  // Necessary, Foo[None] won't compile
  type Unprocessed[_] = None.type
  // Just an alias
  type Processed[X] = Some[X]
}

示例用例:

1
2
3
4
5
6
7
8
9
10
11
12
import Acme._

val raw: Foo[Unprocessed] = Foo[Unprocessed](42,"b", None, None)

def process(unprocessed: Foo[Unprocessed]): Foo[Processed] =
  unprocessed.copy[Processed](c = Some(true), d = Some(42d))

val processed: Foo[Processed] = process(raw)

// No need to pattern match, use directly the x from the Some case class
println(processed.c.x)
println(processed.d.x)

I used this once in my current project. 我遇到的主要问题是当我希望Foo协变时。

或者,如果您不关心T上的绑定:

1
case class Foo[+T[_]](a: Int, b: String, c: T[Boolean], d: T[Double])

然后,当需要Foo[Option]时,可以使用Foo[Unprocessed]Foo[Processed]

1
2
scala> val foo: Foo[Option] = processed
foo: Acme.Foo[Option] = Foo(42,b,Some(true),Some(42.0))


您可以为处理的类型引入一个新的特性,一个扩展该特性的类,以及一个隐式转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case class Foo(bar: Int)

trait HasBaz {
    val baz: Int
}

class FooWithBaz(val foo: Foo, val baz: Int) extends HasBaz

object FooWithBaz {
    implicit def innerFoo(fwb: FooWithBaz): Foo = fwb.foo

    implicit class RichFoo(val foo: Foo) extends AnyVal {
        def withBaz(baz: Int) = new FooWithBaz(foo, baz)
    }
}

所以你可以这样做:

1
2
import FooWithBaz._
Foo(1).withBaz(5)

而且,尽管withBaz返回FooWithBaz,但由于隐式转换,我们可以在必要时将返回值视为Foo


另一个策略可能是创建另一个案例类:

1
2
3
4
5
6
7
8
9
10
11
12
13
case class Foo(
  id: Int,
  bar_id: Int,
  baz_id: Int,
  x: Int,
  y: String
)

case class ProcessedFoo(
  foo: Foo,
  bar: Bar,
  baz: Baz
)