2015-02-24 48 views
4

我正在阅读结构化JSON,使用Play Frameworks'JSON读取以构建具有案例类的对象图。如何从Scala案例类中修饰不可变对象图形

一个例子:

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的每个字段多次,一遍又一遍?

我知道几种方法,觉得不对劲:

  • 使“栏:选项[酒吧]”和“巴兹:选项[巴兹]”案例类的参数,然后我可以使用“复制”使新版本的Foo类与他们设置了某些东西;但之后每次访问时都必须检查它们 - 效率低下,不安全,无法制作DecoratedFoo,只能保证正确的结构
  • 创建第二个案例类,它是所有副本的粘贴在第一个结构中,但添加了两个额外的装饰参数 - 但这意味着回显定义中的整个参数列表,并在创建实例时再次显示它的实例
  • 案例类继承显然是有争议的,并且在任何情况下也会出现无论如何,要求我重复每一个参数,在子类构造函数中?
  • 使非案例超类列出常见案例类参数。然后在case类中扩展它。但是这似乎仍然需要在子类构造函数中重复每个单独的参数。
  • 我看到有人在讨论这个问题,并在运行时使用反射来填充他们的装饰副本的基本属性 - 这避免了回声,但现在你没有类型安全性,将属性名称指定为字符串,开销等。

当然,Scala必须有办法让人们从简单的对象中编写更复杂的不可变对象,而不必手工复制它们的每一部分。

+1

这对我来说也是一个痛点。在我看来,一般问题是声明一个核心数据模型,然后以DRY的方式定义派生/增强模型,这些模型是原始模型的变换。到目前为止,我还没有找到一个通用的解决方案。 – acjay 2015-02-24 15:34:27

回答

1

结合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)) 
+0

我有点害怕斯卡拉,这是可能的。不幸的是我不能编译它。未处理和已处理的类型语句会失败,并显示“期望的类或对象定义”。我注意到,如果我把它们放在Foo {}的主体中,它们会被编译,但那么你的示例用例不起作用,因为已处理/未处理的符号是未知的。 – user2057354 2015-02-27 13:00:00

+0

@ user2057354是的,你应该把类型声明放在一个对象中(而不是在Foo类中)。然后,您可以在需要时导入它们。例如,它可能位于Foo包的包对象或Foo的伴侣对象中。 – Dimitri 2015-02-27 13:07:40

+0

我觉得这很酷。这确实让它编译和运行。但是在我描述的场景中,它不适用于JSON Reads。具有额外的字段/参数会中断读取(“缺少在对象Foo中应用方法的参数” - 因为我现在在Foo上具有不在JSON中的额外字段/属性)。即使我为c或d指定了一个默认的“None”值,这就是这种情况,这看起来是错误的。 – user2057354 2015-03-02 00:16:15

1

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

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

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

我明白了。也许这是所有罪恶中最少的,尽管它要求对间接引用foo。 – user2057354 2015-02-25 16:41:45

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

+0

我很着迷这种方法。但它不能编译,我担心,因为我仍然只是在努力解决你在这里要做的事情,我还无法弄清楚它的问题。 – user2057354 2015-02-27 12:35:59

+0

错误:(13,16)播放2编译器: Foo.scala:13:RichFoo已经被定义为(编译器生成的)方法RichFoo 隐类RichFoo(VAL FOO:美孚)延伸AnyVal { ^ – user2057354 2015-02-27 12:37:19

+0

即错误似乎完全奇怪和无益 - 如果解决方案显而易见,我仍然在学习隐式转换和道歉。 – user2057354 2015-02-27 12:38:18