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)) |
- 我有点惊讶斯卡拉,这是可能的。不幸的是,我无法编译它。用于未处理和处理的类型语句失败,带有"预期的类或对象定义"。我注意到,如果我将它们放入foo它们编译的主体中,但是您的示例用例不起作用,因为处理/未处理的符号未知。
- @用户2057354是的,您应该将类型声明放在对象内(而不是放在foo类内)。然后您可以在需要时导入它们。例如,它可以在foo包的package对象中,或者foo的伴生对象中。
- 我觉得这很酷。这确实使它得以编译和运行。但在我描述的场景中,它不适用于JSON读取。让额外的字段/参数中断读取(对象foo中缺少方法的参数),因为我现在在foo中有额外的字段/属性,而这些字段/属性不在json中)。即使我为C或D指定了一个默认的"无"值,情况也是如此,这似乎是错误的。
- 例如,隐式val foo reads:reads[foo[未处理]]=((uuu "a").read[int]and(uuu "b").read[string])(foo[未处理])…不起作用,可能无法工作?
- @用户2057354使用Reads.pure。比如implicit val fooReads: Reads[Foo[Unprocessed]] = ( (__ \"a").read[Int] and (__ \"b").read[String] __.read(Reads.pure(None)) and __.read(Reads.pure(None)))(Foo[Unprocessed])(可能还需要一些额外的输入)。您还可以编写另一个应用方法:(a, b) => Foo[Unprocessed](a, b, None, None)。
- 真的。好的-我会尽快试试的!我一直在翻阅playframework和reads api的文档(playframework.com/documentation/2.3.x/scalajsoncombinator和playframework.com/documentation/2.3.x/api/scala/&hellip;)—是否有"reads.pure"的地方记录我错过了它?如果有什么东西我应该一直在读,我道歉!
- @用户2057354没有记录,但是它显示在reads特性的伴生对象上(点击顶部的"t"在伴生和类/特性之间切换)。
- 恐怕还是不行。implicit val fooReads: Reads[Foo[Unprocessed]] = ( (__ \"a").read[Int] and (__ \"b").read[String] and __.read(Reads.pure(None)) and __.read(Reads.pure(None)))(Foo[Unprocessed])导致"对象foo中应用方法的参数丢失"。
- 我曾尝试过提供,例如,read[选项[布尔值]](reads.pure(none)),但这不起作用。如果我必须指定一个新的应用方法,并对foo的部分进行冗余的列表,那么我已经对foo的组件进行了一个新的回音(不幸的是,这会破坏这个练习的目的)。
- 啊,我想我找到了答案。必须是implicit val fooReads: Reads[Foo[Unprocessed]] = ( (__ \"a").read[Int] and (__ \"b").read[String] and __.read(Reads.pure(None)) and __.read(Reads.pure(None)))(Foo[Unprocessed] _),因为某种原因,最后一个是造成差异的原因。
您可以为处理的类型引入一个新的特性,一个扩展该特性的类,以及一个隐式转换:
所以你可以这样做:
1 2
| 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)扩展anyval ^"
- 这段代码是用scalac版本2.11.2为我编译的。你在用什么?通过阅读该错误消息,似乎可以使RichFoo不扩展AnyVal可能会有所帮助,但这样会降低一些效率。
- 使用scala 2.11.1。
另一个策略可能是创建另一个案例类:
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
) |
- 我懂了。也许这是最不邪恶的,尽管它需要间接地提到foo。