【问题标题】:Making more "functional" code in Scala to use immutable collections在 Scala 中制作更多“功能性”代码以使用不可变集合
【发布时间】:2013-08-17 00:36:07
【问题描述】:

我正在将一种算法从 Java 移植到 Scala,该算法在 VP Tree 上进行范围搜索。简而言之,树中的节点具有空间坐标和半径:该半径内的节点可以在左子树上找到,而该半径外的节点可以在右子树上找到。范围搜索尝试在树中查找到查询对象指定距离内的所有对象。

在 Java 中,我向函数传递了一个数组列表,它在其中累积结果,可能会向下递归其中一个或两个子树。这是 Scala 的直接端口:

def search(node: VPNode[TPoint, TObject], query: TPoint, radius: Double,
    results: collection.mutable.Set[TObject]) {

  var dist = distance(query, node.point)

  if (dist < radius)
    results += node.obj

  if (node.left != null && dist <= radius + node.radius)
    search(node.left, query, radius, results)

  if (node.right != null && dist >= radius + node.radius)
    search(node.right, query, radius, results)
}

Scala 的默认集合类型是不可变的,我认为一直输入collection.mutable. 有点烦人,所以我开始研究它。似乎建议使用不可变集合几乎总是可以的:尽管我正在使用此代码进行数百万次查找,但在我看来,复制和连接结果数组会减慢速度。

例如this 之类的答案表明需要更“从功能上”解决问题。

那么,我应该怎么做才能以更 Scala 风格的方式解决这个问题?理想情况下,我希望它与 Java 版本一样快,但无论如何我都对解决方案感兴趣(并且总是可以分析它们以查看它是否有很大的不同)。

注意,我才刚刚开始学习 Scala(我想我不妨在一些有用的东西上磨练一下)但我对函数式编程并不陌生,之前使用过 Haskell(尽管我认为我不是那么好在它!)。

【问题讨论】:

  • 为什么在 Java 中使用 ArrayList 而在 Scala 中使用 Set?使用ArrayBuffer 会更相似。
  • 其实我也是用Java的Set,只是说错了

标签: scala data-structures functional-programming


【解决方案1】:

这是我认为更实用的方法:

val emptySet = Set[TObject]()

def search(node: VPNode[TPoint, TObject], query: TPoint, radius: Double): Set[TObject] = {
  val dist = distance(query, node.point)

  val left = Option(node.left) // avoid nulls
    .filter(_ => dist <= radius + node.radius) // do nothing if predicate fails
    .fold(emptySet)(l => search(l, query, radius)) // continue your search

  val right = Option(node.right)
    .filter(_ => dist >= radius + node.radius)
    .fold(emptySet)(r => search(r, query, radius))

  left ++ right ++ (if (dist < radius) Set(node.obj) else emptySet)
}

search 函数不是将您的 mutable.Set 传递给每个 search 函数,而是返回一个 Set[TObject],然后将其连接到其他集合上。如果您要构建函数调用,看起来树的每个节点都在相互连接(假设它们在您的半径范围内)。

从效率的角度来看,这可能不如可变版本高效。使用List 而不是Set 可能会更好,然后您可以在完成后将最终的List 转换为Set(尽管仍然可能不如可变版本快)。

更新 要回答有关好处的问题:

  1. 确定性 - 由于它是不可变的,因此在使用相同参数调用此函数时,您始终可以保证得到相同的结果。话虽如此,您的原始版本应该是确定性的,您只是不知道还有谁在修改您的结果,尽管这可能不是什么大问题。
  2. 难以阅读? - 我认为这更多的是不同风格编程的意见和经验问题。我发现您的版本难以阅读,因为您没有从函数返回任何值并且您有多个 if 语句。我同意一开始Option/filter/fold 可能看起来有点奇怪,但是在你开始使用它们一段时间后(就像任何东西一样)它变得容易阅读。我会将这与能够在 .NET 中读取 LINQ 进行比较。
  3. 性能 - 使用@huynhjl 的答案,使用List,您应该从原始版本中获得相同的性能,如果不是更好的性能。看来您实际上并不需要使用 Set,它需要确保集合中的所有内容都是唯一的。
  4. 垃圾收集 - 在纯功能版本中,您可以快速创建新对象并快速删除它们,这意味着它们很可能无法在 GC 的第一代之后存活。这很重要,因为在代之间移动对象是强制 GC 暂停的原因。在可变版本中,您传递的是对原始集合的引用,该集合的保留时间更长,可能会被压缩到下一代。这并不是最好的例子,因为您的可变版本可能不会长久存在,而且谁知道您想对返回对象做什么(可能会保留一段时间)。在可变版本中,您很可能最终得到指向第二代对象的第二代集合,而在不可变版本中,您最终将得到指向第二代对象的第一代集合。清理不可变版本会更快,更不会停顿(同样,这是对对象的使用和 GC 正在做什么做出一些广泛的假设和概括,您的里程可能会有所不同)。
  5. 并行性 - 功能版本可以轻松并行化,而可变版本则不能。根据树的大小,这可能不是什么大问题。

由于您似乎很感兴趣,我建议您阅读Functional Programming in Scala。它涵盖了所有这些基础知识,我认为这对初学者来说是一种很好的方式。

【讨论】:

  • 我开始写一个答案,然后意识到它与你的模数相同,以 Option/filter/fold 为模,这掩盖了这一事实,所以我在我的回答。也许应该在VPNode 中避免空值。
  • 很有趣,谢谢。我花了一分钟才弄清楚那里发生了什么,我需要掌握这个功能范式!顺便说一句:如果它更难阅读(有争议)而且速度更慢,那么这样写有什么好处?
  • @huynhjl 我假设他正在使用一个使用 null 的 java 库,否则是的,在 VPNode 中避免 null 会很棒。使用 scala 的 TreeSetTreeMap 您可以轻松地在树上使用 fold 或实现一些自定义的 fold 操作。作为旁注,我喜欢你的带有List 累加器的版本。我从那个版本开始,但最终选择了另一条路线。
  • 很好的答案,感谢您推荐这本书。并行化是我将要研究的东西,这点很好。
【解决方案2】:

我想知道使用标准不可变List 是否会获得良好的性能。 search 所做的只是一次检查一个节点,如果当前元素满足某些条件,则追加它,然后进行双重递归。所以你可以使用不可变的累加器:

def search(node: VPNode[TPoint, TObject], query: TPoint, radius: Double,
    acc: List[TObject] = Nil): List[TObject] = {

  val dist = distance(query, node.point)
  val mid = if (dist < radius) node.obj :: acc else acc

  val midLeft =
    if (node.left != null && dist <= radius + node.radius)
      search(node.left, query, radius, mid)
    else mid

  if (node.right != null && dist >= radius + node.radius)
    search(node.right, query, radius, midLeft)
  else midLeft
}  

据我所知,这只是累加器的开头,应该很快。

请注意,我认为可以在内部使用可变集合并将不可变集合返回给调用者:

def search(node: VPNode[TPoint, TObject], query: TPoint, radius: Double): Vector[TObject] = {
  import collection.immutable.{VectorBuilder => Builder}
  def rec(n: VPNode[TPoint, TObject], acc: Builder[TObject]): Builder[TObject] = {
    val dist = distance(query, node.point)
    val mid = if (dist < radius) acc += node.obj
    if (node.left != null && dist <= radius + node.radius) rec(node.left, acc)
    if (node.right != null && dist >= radius + node.radius) rec(node.right, acc)
    acc
  }
  rec(node, new Builder()).result
} 

【讨论】:

  • 很好,我不知道你可以做一些这样的事情。我认为如果它只是附加到前面,那么一个不可变的集合可能是前进的方向。
  • @stewartml,如果你需要使用Set,你可以使用不可变集并做acc + node.obj。如果您最终执行了一些分析/基准测试,请告诉我它与 Java 版本的比较。
猜你喜欢
  • 2023-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-27
  • 2016-01-24
  • 1970-01-01
  • 2013-08-30
  • 1970-01-01
相关资源
最近更新 更多