在scala中,如何创建包含引用的不可变对象列表?

在scala中,如何创建包含引用的不可变对象列表?,scala,Scala,下面的示例构建了一组人员,他们之间有家庭链接 case class Person(id: Int, name: String, father: Option[Int], mother: Option[Int], children: Set[Int]) val john = Person(0, "john", None, None, Set(2)) val maria = Person(1, "maria", None, None, Set(2)) val georges = P

下面的示例构建了一组人员,他们之间有家庭链接

  case class Person(id: Int, name: String, father: Option[Int], mother: Option[Int], children: Set[Int])

  val john = Person(0, "john", None, None, Set(2))
  val maria = Person(1, "maria", None, None, Set(2))
  val georges = Person(2, "georges", Some(0), Some(1), Set.empty)

  val people = Set(john, maria, georges)
  val peopleMap = people.map(p => (p.id, p)).toMap

  val meanChildrenSize = people.map(p => p.children.map(peopleMap).size).sum.toDouble / people.size
这个例子工作正常,但我不喜欢这样,我需要构建这个额外的
peopleMap
,并调用
p.children.map(peopleMap)
,因为它使阅读变得困难。我更愿意将示例建模如下:

case class Person(id: Int, name: String, father: Option[Person], mother: Option[Person], children: Set[Person])

val john = Person(1, "john", None, None, Set.empty)
val maria = Person(2, "maria", None, None, Set.empty)
val georges = Person(3, "georges", Some(john), Some(maria), Set.empty)

val people = Set(john, maria, georges)

val meanChildrenSize = people.map(p => p.children.size).sum.toDouble / people.size
但是,现在的问题是john和maria无法初始化子集合,因为georges尚未创建。如何解决这个问题(最好使用不可变的case类)


更新: 有人提议使用lazy:

  case class Person(id: Int, name: String, father: Option[Person], mother: Option[Person], children: Set[Person])

  lazy val john: Person = Person(1, "john", None, None, Set(georges))
  lazy val maria: Person = Person(2, "maria", None, None, Set(georges))
  lazy val georges: Person = Person(3, "georges", Some(john), Some(maria), Set.empty)

StackOverflower错误导致此操作失败。

完全不可变的案例类无法解决此问题,因为您需要
lazy val
s在不可变的数据结构中创建循环依赖项

以下是定义提供所需API的
Person
类的一种方法:

class Person(val id: Int, val name: String, father0: => Option[Person], mother0: => Option[Person], children0: => Set[Person]) {
  lazy val father = father0
  lazy val mother = mother0
  lazy val children = children0

  override def equals(that: Any): Boolean = that match {
    case Person(this.id, this.name, this.father, this.mother, this.children) => true
    case _ => false
  }

  override def hashCode(): Int = { ... }
}

object Person {
  def apply(id: Int, name: String, father0: => Option[Person], mother0: => Option[Person], children0: => Set[Person]): Person =
    new Person(id, name, father0, mother0, children0)

  def unapply(p: Person): Some[(Int, String, Option[Person], Option[Person], Set[Person])] =
    Some(p.id, p.name, p.father, p.mother, p.children)
}

不幸的是,您不能直接使用case类这一事实意味着您有相当多的样板要编写。

我认为@srjd的答案虽然不完整,也有问题,但是朝着正确方向迈出的一步

对于更完整的工作解决方案,您可以使用

class Person(id: Int, name: String, mother: => Option[Person], father: => Option[Person], children: => Set[Person]) {
  def getId = id
  def getName = name
  def getMother = mother
  def getFather = father
  def getChildren = children

  override def equals(that: Any): Boolean = that match {
    case Person(tId, tName, tMother, tFather, tChildren) => {
      id == tId && name == tName && mother == tMother && father == tFather && children == tChildren
    }
    case _ => false
  }

  // you will want a better hashcode method
  override def hashCode(): Int = List(id, name).hashCode()

}

object Person {

  def apply(id: Int, name: String, mother: => Option[Person], father: => Option[Person], children: => Set[Person]): Person =
    new Person(id, name, mother, father, children)

  def unapply(p: Person): Option[(Int, String, Option[Person], Option[Person], Set[Person])] =
    Some((p.getId, p.getName, p.getFather, p.getMother, p.getChildren))
}
现在你可以像下面这样使用它

lazy val father: Person = Person(1, "father", None, None, Set(son))

lazy val mother: Person = Person(2, "mother", None, None, Set(son))

lazy val son: Person = Person(2, "son", Some(father), Some(mother), Set.empty[Person])

val people = Set[Person](father, mother, son)

val totalChildrenSize1 = people
  .map({
    case Person(id, name, mother, father, children) => children.size
  })
  .sum
  .toDouble

// Or
val totalChildrenSize2 = people
  .map(p => p.getChildren.size)
  .sum
  .toDouble

val meanChildrenSize1 = totalChildrenSize1 / people.size
// Or
val meanChildrenSize2 = totalChildrenSize2 / people.size

再懒一点会有帮助的。让我们从极端情况开始:

case class Person(id: Int, name: String, father: () => Option[Person], mother: () => Option[Person], children: () => Set[Person])

lazy val john = Person(1, "john", () => None, () => None, () => Set.empty)
lazy val maria = Person(2, "maria", () => None, () => None, () => Set.empty)
lazy val georges = Person(3, "georges", () => Some(john), () => Some(maria), () => Set.empty)

val people = Set(john, maria, georges)
实验:

val meanChildrenSize = people.map(p => p.children().size).sum.toDouble / people.size 
meanChildrenSize: Double = 0.0
val meanChildrenSize = people.map(p => p.children().size).sum.toDouble / people.size 
meanChildrenSize: Double = 0.3333333333333333
这将允许您避免关系中的循环,但可能会破坏“equals”契约(在数据中使用循环将破坏naive
equals
-方法并导致堆栈溢出,因此最好不要为function0
()=>…
成员实现它)-请参见下面的示例。它还将导致重新评估问题(这可以通过@srjd方法的名称调用来解决,但是,如果
def max:Person=Person(1,“aaa”,None,None,Set(max));max==max,则该方法可能会堆叠溢出等式)

因此,更实际的方法是:

case class Person(id: Int, name: String, father: Option[Person], mother: Option[Person], children: Set[Person])

lazy val john = Person(1, "john", None, None, Set.empty)
lazy val maria = Person(2, "maria", None, None, Set.empty)
lazy val georges = Person(3, "georges", Some(john), Some(maria), Set.empty)

val people = Set(john, maria, georges)
实验得到了同样的结果。但是,一些复杂的交叉依赖可能会导致问题,具体取决于
人员
集中的初始化顺序(我应该提到,任何顺序都适用于您当前的示例和我对数据的随机非循环修改)

或者,正如@YuvalItzchakov所建议的,您可以先初始化
georges
,但它的可伸缩性较差,因为您需要考虑初始化顺序

lazy val
方法不起作用时的示例:

//John is his own child now (almost like Fry from Futurama)
lazy val john: Person = Person(1, "john", None, None, Set(john))
val people = Set(georges,john, maria) 
java.lang.StackOverflowError
因此,如果您希望数据中出现一些循环,最好使用上面介绍的“极端”方法:

...
//John is his own child now
lazy val john: Person = Person(1, "john", () => None, () => None, () => Set(john))
val people = Set(john, maria, georges) 
实验:

val meanChildrenSize = people.map(p => p.children().size).sum.toDouble / people.size 
meanChildrenSize: Double = 0.0
val meanChildrenSize = people.map(p => p.children().size).sum.toDouble / people.size 
meanChildrenSize: Double = 0.3333333333333333
因此,结果是正确的,但如果手动重写等于account for father/mother/children成员,则会产生堆栈溢出。您的模型建议id相等,因此您可以使用这样的smthng:

 case class Person(id: Int, name: String, father: () => Option[Person], mother: () => Option[Person], children: () => Set[Person]){
   override def equals(that: Any): Boolean = that match {
     case Person(id2, name2, _, _, _) => id == id2 && name == name2
     case _ => false
   }
   //You might not need to override hashCode (if you're not gonna put data as a key into some big dictionaries) as `equals` is stronger
   override def hashCode = (id, name).hashCode
 }
例如:

 val a = Person(1, "john", () => None, () => None, () => Set()) == Person(1, "john", () => None, () => None, () => Set()) 
 val b = a

 @ Map(a -> "a", b -> "b") 
 res55: Map[Person, String] = Map(Person(1, "john", <function0>, <function0>, <function0>) -> "b")
 @ res55(a) 
 res56: String = "b"
 @ res55(b) 
 res57: String = "b"
您可能会注意到,您的方法确实考虑了重复的儿童尺寸(!),因此您可能需要:

val meanChildrenSize = people.toList.map(_.children().size).sum.toDouble / people.size
meanChildrenSize: Double = 0.6666666666666666

为什么不先创建
georges
,然后从
georges
中创建的引用绑定
john
maria
?这实际上有更多的问题
mother
每次调用
getMother
@Jasper-M时都会重新评估。如果您没有阅读两个答案,请现在阅读,并注意两种方法在这方面是相同的。与sjrd的答案相比,这个答案只是一个更完整的无问题改进。这会增加多少开销?作为
母亲
只是
=>实例
。请记住,正常的getter也是实例。为什么会被否决?有人吗?这不仅仅是开销的问题。还有副作用。嗯。。。副作用是Scala FP的祸根。Scala中几乎所有的FP构造都可以被认为是不安全的,因为
可能会引入副作用
@Jasper-M以及我对StackOverFlow的看法。。。当你发现一个重大问题的答案不可接受时,应该使用否决票。当你喜欢某件事的答案时,请举手表决。在其他情况下,答案应该被搁置。为什么这一点会被否决?我最近注意到这里有一个讨厌的习惯,就是对答案进行向下投票,不提供随时可以复制粘贴的代码。我看不出这是如何解决问题的。如何初始化这3个人?
lazy val john:Person=…
,等等。我看不出这是如何解决问题的。john.children.size仍然是0,应该是1(乔治)。谢谢,回答被接受。下面是一个相关的问题: