【问题标题】:How to decorate an immutable object graph from scala case classes如何从 scala 案例类中装饰不可变对象图
【发布时间】:2015-04-26 04:01:12
【问题描述】:

我正在阅读结构化 JSON,使用 Play Frameworks 的 JSON 读取来构建带有案例类的对象图。

一个例子:

case class Foo (
                       id: Int,
                       bar_id: Int,
                       baz_id: Int,
                       x: Int,
                       y: String
                       )
{
  var bar: Bar = null
  var baz: Baz = null
}

在构建 Foo 之后,我必须稍后再回来通过设置 bar 和 baz 来装饰它。这些是在其他 JSON 文件中定义的,并且仅在所有解析完成时才知道。但这意味着 Foo 不能是不可变的。

在 Scala 中创建不可变对象的“正确”方法是什么,然后是它的修饰版本,而不是多次重复 Foo 的每个字段,一遍又一遍?

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

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

Scala 肯定有办法让人们从简单的对象中组合出更复杂的不可变对象,而不必手动复制它们的每一部分?

【问题讨论】:

  • 这也是我的痛点。在我看来,一般问题是声明一个核心数据模型,然后以 DRY 的方式定义作为原始模型转换的派生/增强模型。到目前为止,我还没有找到该问题的通用解决方案。

标签: json scala playframework immutability case-class


【解决方案1】:

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

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)
    }
}

那么你可以这样做:

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

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

【讨论】:

  • 我对这种方法很着迷。但它无法编译,而且我担心,因为我仍在努力解决你在这里尝试做的事情,所以我还无法弄清楚它的问题。
  • Error:(13, 16) Play 2 Compiler: Foo.scala:13: RichFoo is already defined as (compiler-generated) method RichFoo implicit class RichFoo(val foo: Foo) extends AnyVal { ^
  • 这个错误看起来非常奇怪且无益 - 我仍在学习隐式转换,如果解决方案很明显,我道歉?
  • 隐式类必须在某个其他类或对象中。我认为这可能是导致错误的原因。我在 REPL 中玩过这个,其中隐式规则略有不同。现在看看我的更改(我将RichFoo 移动到FooWithBaz 对象中)。
  • 恐怕还是编译不出来。我收到“错误:(16, 20) 值类可能不是另一个类的成员隐式类 RichFoo(val foo: Foo) extends AnyVal { ^"
【解决方案2】:

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

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]
}

示例用例:

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)

我在当前项目中使用过一次。我遇到的主要问题是我希望Foo 是协变的。


或者,如果您不关心T 上的界限:

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

那么您可以在需要Foo[Option] 时使用Foo[Unprocessed]Foo[Processed]

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

【讨论】:

  • 我对 Scala 感到有点惊讶,这是可能的。不幸的是,我无法编译它。您的 Unprocessed 和 Processed 类型语句因“预期的类或对象定义”而失败。我注意到如果我将它们放在它们编译的 Foo {} 的主体中,但是您的示例用例不起作用,因为已处理/未处理的符号是未知的。
  • @user2057354 是的,您应该将类​​型声明放在一个对象中(而不是放在 Foo 类中)。然后,您可以在需要时导入它们。例如,它可以在 Foo 的包的包对象中,也可以在 Foo 的伴随对象中。
  • 我觉得这太酷了。这确实让它编译和运行。但在我描述的场景中,它不适用于 JSON 读取。拥有额外的字段/参数会破坏读取(“在对象 Foo 中应用的方法缺少参数” - 因为我现在在 Foo 上有额外的字段/属性,但不在 JSON 中)。即使我为 c 或 d 指定默认的“无”值也是如此,这似乎是错误的。
  • 例如隐式 val fooReads: Reads[Foo[Unprocessed]] = ( (__ \ "a").read[Int] and (__ \ "b").read[String] )(Foo[Unprocessed]) ...doesn'不工作,也许不能工作?
  • @user2057354 使用Reads.pureimplicit val fooReads: Reads[Foo[Unprocessed]] = ( (__ \ "a").read[Int] and (__ \ "b").read[String] __.read(Reads.pure(None)) and __.read(Reads.pure(None)))(Foo[Unprocessed]) 之类的东西(可能需要额外输入)。你也可以写另外一个apply方法:(a, b) =&gt; Foo[Unprocessed](a, b, None, None).
【解决方案3】:

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

case class Foo(
  id: Int,
  bar_id: Int,
  baz_id: Int,
  x: Int,
  y: String
)

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

【讨论】:

  • 我明白了。也许这是所有弊端中最小的一个,尽管它需要对 foo 的每次引用都进行间接引用。
猜你喜欢
  • 2012-05-15
  • 1970-01-01
  • 1970-01-01
  • 2019-05-08
  • 2011-02-03
  • 2012-10-04
  • 1970-01-01
  • 1970-01-01
  • 2011-03-28
相关资源
最近更新 更多