学习如何在Scala中进行动态编程,我经常发现自己处于一种情况,我希望递归地继续遍历数组(或其他一些可迭代的)项目。当我这样做时,我倾向于编写像这样繁琐的函数:
def arraySum(array: Array[Int], index: Int, accumulator: Int): Int => {
if (index == array.length) {
accumulator
} else {
arraySum(array, index + 1, accumulator + array(index)
}
}
arraySum(Array(1,2,3), 0, 0)
(暂时忽略我可以在数组上调用sum
或做.reduce(_ + _)
,我正在尝试学习编程原理。)
但这似乎我传递了很多变量,将数组传递给每个函数调用到底是什么意思?这似乎是不洁净的。
所以相反我有了用迭代器做这个的想法,而不用担心传递索引:
def arraySum(iter: Iterator[Int])(implicit accumulator: Int = 0): Int = {
try {
val nextInt = iter.next()
arraySum(iter)(accumulator + nextInt)
} catch {
case nee: NoSuchElementException => accumulator
}
}
arraySum(Array(1,2,3).toIterator)
这似乎是一个更清洁的解决方案。但是,当您需要使用动态编程来探索某些结果空间并且不需要在每个函数调用时调用迭代器时,这就会崩溃。 E.g。
def explore(iter: Iterator[Int])(implicit accumulator: Int = 0): Int = {
if (someCase) {
explore(iter)(accumulator)
} else if (someOtherCase){
val nextInt = iter.next()
explore(iter)(accumulator + nextInt)
} else {
// Some kind of aggregation/selection of explore results
}
}
我的理解是iter
迭代器在这里作为传递引用,所以当这个函数调用iter.next()
时,它会改变传递给函数的所有其他递归调用的iter实例。因此,为了解决这个问题,现在我在每次调用explore
函数时克隆迭代器。 E.g:
def explore(iter: Iterator[Int])(implicit accumulator: Int = 0): Int = {
if (someCase) {
explore(iter)(accumulator)
} else if (someOtherCase){
val iterClone = iter.toList.toIterator
explore(iterClone)(accumulator + iterClone.next())
} else {
// Some kind of aggregation/selection of explore results
}
}
但这看起来非常愚蠢,当我有多个迭代器时,愚蠢性会升级,这些迭代器在多个else if
个案例中可能需要也可能不需要克隆。处理这种情况的正确方法是什么?我怎样才能优雅地解决这些问题?
答案 0 :(得分:1)
假设您要编写一个需要一些复杂数据结构作为参数的反向跟踪递归函数,以便递归调用接收稍微修改过的数据结构版本。您可以通过以下几种方式进行操作:
我想详细说明最后一点,因为这是在Scala和函数式编程中首选的方法。
以下是您使用第三种策略的原始代码:
def arraySum(array: Array[Int], index: Int, accumulator: Int): Int = {
if (index == array.length) {
accumulator
} else {
arraySum(array, index + 1, accumulator + array(index))
}
}
如果您使用List
代替Array
,则可以将其重写为:
@annotation.tailrec
def listSum(list: List[Int], acc: Int): Int = list match {
case Nil => acc
case h :: t => listSum(t, acc + h)
}
此处,h :: t
是将列表解构为head
和tail
的模式。
请注意,您不再需要显式索引,因为访问列表的尾t
是一个常量操作,因此只有相关的剩余子列表才会传递给{{1}的递归调用}。
这里没有回溯,但如果递归方法会回溯,使用列表会带来另一个好处:提取子列表几乎是免费的(恒定时间操作),但它仍然保证是不可变的,所以你可以传递它进入递归调用,而不必关心递归调用是否修改它,因此您不必做任何事情来撤消递归调用可能已经完成的任何修改。这是持久不可变数据结构的优点:相关列表可以共享其大部分结构,同时仍然从外部显示为不可变,因此不可能仅仅因为您可以访问此列表的尾部而破坏父列表中的任何内容。对于可变数组的视图,情况并非如此。