How to decorate an immutable object graph from scala case classes
我正在阅读结构化JSON,使用PlayFrameworks的JSON reads构建一个包含case类的对象图。
一个例子:
| 12
 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类,并静态跟踪处理的字段是否为空:
| 12
 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]
} | 
示例用例:
| 12
 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]。
| 12
 
 | 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] _),因为某种原因,最后一个是造成差异的原因。
 
	 
您可以为处理的类型引入一个新的特性,一个扩展该特性的类,以及一个隐式转换:
所以你可以这样做:
| 12
 
 | 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。
 
	 
另一个策略可能是创建另一个案例类:
| 12
 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。