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

时间:2017-01-18 08:58:45

标签: 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 = 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

但是,现在的问题是约翰和玛丽亚无法初始化子集,因为仍然没有创建乔治。如何解决这个问题(最好使用不可变的案例类)?

更新: 有人建议使用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)

这会因StackOverflowError而失败。

3 个答案:

答案 0 :(得分:1)

多一点懒惰会有所帮助。让我们从极端情况开始:

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

这可以让你避免关系中的循环,但可能会破坏“等于”契约(在数据中有循环会破坏天真的equals - 方法并导致StackOverflow,所以最好不要为function0 {{1}实现它成员) - 见下面的例子。它也会导致重新评估问题(可以通过@ srjd-approach的call-by-name解决,但是在() => ...的情况下,该方法可能会堆栈溢出相等。)

因此,更实际的方法是:

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建议 - 您可以先初始化people,但它的可扩展性较低,因为您必须关心初始化顺序。

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) 

所以结果是正确的,但如果你手动覆盖equals到父/母/子成员的帐户 - 它会产生堆栈溢出。你的模型通过id表示相等,所以你可以像这样使用smthng:

val meanChildrenSize = people.map(p => p.children().size).sum.toDouble / people.size 
meanChildrenSize: Double = 0.3333333333333333

示例:

 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" - res55-Map会有两个元素而不是一个元素。您可能还需要“覆盖”apply / unapply(如@sjrd建议的那样)以避免override hashCode - 成员(或者只是在与function0模式匹配时忽略它们)。顺便说一句,你也可以将这种微弱的平等调整为@srjd方法。

作为结论,如果您从某些文本表示或数据库构建数据 - 您可以依赖外部模型的“非循环”(或在验证阶段检查它)并完全避免使用“极端”方法。

P.S。小心懒惰的val,它们实际上已经在内部“同步”(它应该在新版本的Scala中更改,但至少在Scala 2.11.x中没有更改),因此在复杂情况下可能出现死锁 - 通常,我建议避免_内的任何手动锁。

更新(回答问题的更新):

如果您需要让父母/子女双方联系(循环),您可以将两种解决方案结合起来:

lazy val

你可能会注意到你的方法确实考虑了重复儿童的大小(!),所以也许你需要这个:

final 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
  }
  override def hashCode = (id, name).hashCode
}


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

val people = Set(john, maria, georges)

val meanChildrenSize = people.map(_.children().size).sum.toDouble / people.size

meanChildrenSize: Double = 0.3333333333333333

答案 1 :(得分:0)

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

以下是定义提供所需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)
}

不能直接使用案例类的事实意味着你有很多样板文件要写,不幸的是。

答案 2 :(得分:-1)

我认为@ 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