Scala中序列计算结果的变化

介绍

刚接触Scala的学生的一个常见问题是:在列表上使用map函数,for表达式和foreach循环之间有什么区别? 关于此问题的主要困惑之一是,Scala中的for表达式不等同于Python和Java等语言中的for循环 -而是,Scala中的foreach等同于for循环。 这种区别突出了理解返回值的含义与依靠副作用执行某些计算的重要性。 它还有助于加强关于固定变量与可重新分配变量以及不可变变量与可变数据结构的一些观点。

任务及其功能解决方案

为了说明这一点,让我们考虑一个简单的任务。 给定一个单词列表 ,计算两个列表:一个列出每个单词的长度,第二个表明单词是否以大写字母开头。 例如,从下面的列表开始。

scala> val words = List("This", "is", "a", "list", "of", "English", "words", ".")
words: List[java.lang.String] = List(This, is, a, list, of, English, words, .)

我们可以通过映射单词列表来计算两个列表,如下所示。

scala> words.map(_.length)
res0: List[Int] = List(4, 2, 1, 4, 2, 7, 5, 1)

scala> words.map(_(0).isUpper)
res1: List[Boolean] = List(true, false, false, false, false, true, false, false)

就是这样了。 但是,我们无需使用对map函数的不同调用(或多个foreach循环,如下所示)就可以做到这一点。 最简单的方法是将每个单词映射到一个元组,该元组包含长度和指示第一个字符是否大写的布尔值。 这将产生一个元组列表,我们将其解压缩以获得一个Lists元组。

scala> val (wlengthsMapUnzip, wcapsMapUnzip) =
|   words.map(word => (word.length, word(0).isUpper)).unzip
wlengthsMapUnzip: List[Int] = List(4, 2, 1, 4, 2, 7, 5, 1)
wcapsMapUnzip: List[Boolean] = List(true, false, false, false, false, true, false, false)

这里的关键是map函数将List [String ]词转换为List [(Int,Boolean)] -也就是说它返回一个值。 我们可以将该值分配给变量,也可以通过对其调用解压缩立即使用它,这反过来会返回一个值为Tuple2(List [Int],List [Boolean])的值
在继续之前,让我们定义一个简单的函数来显示执行此计算的结果,我们将以各种方式进行此操作(并且所有这些都产生完全相同的结果)。

def display (intro: String, wlengths: List[Int], wcaps: List[Boolean]) {
  println(intro)
  println("Lengths: " + wlengths.mkString(" "))
  println("Caps: " + wcaps.mkString(" "))
  println
}

通过如上所述的映射和解压缩结果调用此函数,我们得到以下输出。

scala> display("Using map and unzip.", wlengthsMapUnzip, wcapsMapUnzip)
Using map and unzip.
Lengths: 4 2 1 4 2 7 5 1
Caps: true false false false false true false false

好的,现在让我们开始努力吧。 而不是映射到原始列表,我们将使用foreach遍历该列表,并执行副作用计算以构建两个结果序列。 这种事情通常是在带有for循环的非功能语言中完成 ,因此在Scala中使用了foreach 。 我们将依次探索其中的每一个。

第二种变体:使用可重新分配的不可变列表

我们可以使用可重新分配的变量,这些变量被初始化为空列表,然后在遍历单词列表时添加到它们之前。 因此,我们使用的变量类型为List ,这是一个不变的序列数据结构,但是每次通过循环时,它的值都会重新分配。

var wlengthsReassign = List[Int]()
var wcapsReassign = List[Boolean]()
words.foreach { word =>
  wlengthsReassign = word.length :: wlengthsReassign
  wcapsReassign = word(0).isUpper :: wcapsReassign
}

display("Using reassignable lists.", wlengthsReassign.reverse, wcapsReassign.reverse)

请注意,我们通过添加前缀来构建列表,这意味着它们以相反的顺序退出循环,因此在显示之前必须将其反转。 当然,您可以通过创建单例列表并将两个列表与:::运算符连接来追加到列表。

scala> val foo = List(4,2)
foo: List[Int] = List(4, 2)

scala> foo ::: List(7)
res0: List[Int] = List(4, 2, 7)

但是,不建议这样做,因为它在计算上很昂贵。 将元素添加到列表的前面(左侧)是固定时间操作,而连接两个列表所需的时间与第一个列表的长度成比例。 在处理包含数千个元素的列表之前,这似乎没什么大不了的,然后您会发现相同的代码先经过多次重复然后反转,比使用上述策略追加的代码要快得多。

第三种变化:使用不可分配的,可变的(可增长的)ListBuffer

接下来,我们可以使用ListBuffer,它是一种可变序列数据结构,也恰好支持恒定时间追加操作。 因此,我们可以将其声明为val ,然后使用append方法对序列进行突变,使其在末尾具有新元素。 因此,引用序列的变量不是可重新分配的,但是它们的值是可变的。

import collection.mutable.ListBuffer
val wlengthsBuffer = ListBuffer[Int]()
val wcapsBuffer = ListBuffer[Boolean]()
words.foreach { word =>
  wlengthsBuffer.append(word.length)
  wcapsBuffer.append(word(0).isUpper)
}

display("Using mutable ListBuffer.", wlengthsBuffer.toList, wcapsBuffer.toList)

请注意,我们必须将ListBuffers转换为Lists才能显示调用,以便具有正确的类型作为该函数的参数。
由于它们可以有效地增长(即变长),因此ListBuffers是许多问题的好选择,在这些问题中我们需要累积一组结果,尤其是当我们不知道要累积多少结果时。 但是,如果您知道要累积的结果数,那么使用数组可能会更好,如下所示。

第四种变体:使用不可分配,可变(但长度固定)的数组

对于来自Java的人们,以上两种选择都可能看起来有些奇怪。 在Java中,您更有可能执行命令式解决方案,其中涉及初始化与单词长度相同的数组,然后适当地填充相应的索引。 为此,请使用Array.fill( lengthOfArray )( initialValue

val wlengthsArray = Array.fill(words.length)(0)
val wcapsArray = Array.fill(words.length)(false)
  words.indices.foreach { index =>
  wlengthsArray(index) = words(index).length
  wcapsArray(index) = words(index)(0).isUpper
}

display("Using iteration and arrays.", wlengthsArray.toList, wcapsArray.toList)

我们遍历索引,并为每个索引计算值,并将其分配给相应Array中的适当索引。 同样,我们需要在调用display之前将结果转换为Lists。 indexs方法完全符合您的期望-它为您提供了List的索引。

scala> words.indices
res2: scala.collection.immutable.Range = Range(0, 1, 2, 3, 4, 5, 6, 7)

上面的foreach循环的问题在于,它需要索引到List中,这通常是一个坏主意。 为什么? 因为从列表中获得第i个项目需要与i操作成比例的时间。 为什么? 因为获取具有特定索引i的项的实现涉及剥离列表的头部以得到其尾部,然后寻找尾部的i-1项,因此需要剥离其头部然后寻找第i-2个项目,依此类推。 因此,如果要在列表中获得第10000个项目,则必须执行10,000次操作才能获得它。 如果单词列表中有10,000个元素,您现在可以看到您仅在foreach上执行了10,000个基本计算,并且对每个元素执行2 * index运算以使单词位于该索引处,这意味着对该单词进行20,000个运算仅最后一个索引。
请注意,索引到数组是固定时间操作,因此上述循环中分配的左侧没有问题。
您可能会认为,先存储单词然后再使用两次,可以做得更好,例如

words.indices.foreach { index =>
  val word = words(index)
  wlengthsArray(index) = word.length
  wcapsArray(index) = word(0).isUpper
}

这样比较好,但是只为我们节省一半的操作。 由于我们之前非常乐于遍历单词本身,因此实际上我们不必进行查找-我们可以通过使用可重新分配的计数器索引来实现更好的工作,该索引允许我们将值设置为新数组中的正确位置正在创建。

val wlengthsArray2 = Array.fill(words.length)(0)
val wcapsArray2 = Array.fill(words.length)(false)
var index = 0
words.foreach { word =>
  wlengthsArray2(index) = word.length
  wcapsArray2(index) = word(0).isUpper
  index += 1
}

由于这种模式相当普遍,因此Scala在名为zipWithIndex的序列上提供了一种便捷的方法,该方法返回原始元素及其索引配对的列表。

scala> words.zipWithIndex
res3: List[(java.lang.String, Int)] = List((This,0), (is,1), (a,2), (list,3), (of,4), (English,5), (words,6), (.,7))

这样,我们可以在此类对上进行foreach循环。 在这些情况下,通过在成对条件使用大小写匹配,可以在foreach循环中使用模式匹配功能,如下所示。

val wlengthsArray3 = Array.fill(words.length)(0)
val wcapsArray3 = Array.fill(words.length)(false)
words.zipWithIndex.foreach { case(word,index) =>
  wlengthsArray3(index) = word.length
  wcapsArray3(index) = word(0).isUpper
}

重要的是要了解正在使用的操作的成本,尤其是在循环上下文中,在这种情况下,您固有地多次执行相同的基本操作。

索引序列(向量)

值得指出的是,当您需要一个允许有效索引的不可变序列时,应使用向量。

scala> val bar = Vector(1,2,3)
bar: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)

如果您手头有一个List但想要重复对其进行索引,则可以使用toIndexedSeq将其转换为Vector

scala> val numbers = List(4,9,9,2,3,8)
numbers: List[Int] = List(4, 9, 9, 2, 3, 8)

scala> numbers.toIndexedSeq
res5: scala.collection.immutable.IndexedSeq[Int] = Vector(4, 9, 9, 2, 3, 8)

IndexedSeq是序列的超类型,旨在高效地建立索引,并且当您在List上调用toIndexedSeq时, Vector是默认的“后备”实现。
当然,如果只按顺序遍历序列中的所有元素,则列表可能会更可取,因为它们的开销少了一些,并且它们具有匹配语句中模式匹配的一些不错的属性。

使用预定义功能在序列上进行映射

值得指出的另一件事是,如果您有预定义的函数,则可以将其作为map的参数传递,这可以导致此任务的代码非常简洁。 假设您定义了一个函数,该函数采用String并生成其参数长度以及是否以大写字母开头的元组

def getLengthAndUpper = (word: String) => (word.length, word(0).isUpper)

这样,使用此功能在单词上映射以获得所需列表的代码就很干净了。

val (wlengthsFunction, wcapsFunction) = words.map(getLengthAndUpper).unzip

当然,只有在其他地方需要相同功能的情况下,才可能这样做。 如果不是这样,最好像本博客文章中的第一个地图示例中那样使用匿名函数。 但是,您会看到,如果您拥有类似这样的简单函数库,则现在可以通过在映射到不同列表时重用它们来开始编写更清晰,更简单的代码。

对于表达

注意,之前的循环都是foreach循环,而Java程序员和Pythonistas将用于for循环。 Scala没有for 循环 -它 表达式 。 那么一个常见的问题是:有什么区别? 什么是for表达式,为什么它不是for循环? 区别在于, 表达式返回一个值 ,因此,虽然foreach允许您浏览序列并对每个元素进行一些操作,而for表达式则允许您为每个元素返回一个值。 考虑以下内容,其中我们得出 List [Int]中每个整数的平方。

scala> val numbers = List(4,9,9,2,3,8)
numbers: List[Int] = List(4, 9, 9, 2, 3, 8)

scala> for (num <- numbers) yield num*num
res6: List[Int] = List(16, 81, 81, 4, 9, 64)

我们得到一个结果,而一个foreach循环只是进行计算而什么也不返回。

scala> numbers.foreach { num => num * num }

关键是我们为for表达式中的每个元素产生一个值。 在这种情况下,它基本上等效于使用map 。 这里是在运行单词示例的上下文中。

val (wlengthsFor, wcapsFor) =
  (for (word <- words) yield (word.length, word(0).isUpper)).unzip

display("Using a for expression.", wlengthsFor, wcapsFor)

说完这些,事实证明您可以将for表达式用作循环,而无需返回任何值,例如,如下所示。

scala> for (num <- numbers) { println(num*num) }
16
81
81
4
9
64

我认为通常在这种情况下使用foreach循环会更好,这样很明显,您只在执行副作用,例如打印,重新分配var变量的值或修改可变数据结构。 但是,在某些情况下,例如在遍历多个列表并执行各种过滤操作时,使用for表达式会更方便。 这是一个简短的示例来说明这一点。 给定两个列表,我们可以枚举所有元素的叉积

scala> val numbers = List(4,9,9,2,3,8)
numbers: List[Int] = List(4, 9, 9, 2, 3, 8)

scala> val letters = List('a','C','f','d','z')
letters: List[Char] = List(a, C, f, d, z)

scala> for (n <- numbers; l <- letters) print("(" + n + "," + l + ") ")
(4,a) (4,C) (4,f) (4,d) (4,z) (9,a) (9,C) (9,f) (9,d) (9,z) (9,a) (9,C) (9,f) (9,d) (9,z) (2,a) (2,C) (2,f) (2,d) (2,z) (3,a) (3,C) (3,f) (3,d) (3,z) (8,a) (8,C) (8,f) (8,d) (8,z)

您还可以根据这些值进行过滤,以将输出限制为inter(est。

scala> for (n <- numbers; if (n>4); l <- letters) print("(" + n + "," + l + ") ")
(9,a) (9,C) (9,f) (9,d) (9,z) (9,a) (9,C) (9,f) (9,d) (9,z) (8,a) (8,C) (8,f) (8,d) (8,z)

还有更多的这一点,但我会离开这里,因为使用这种方式表达被用于在其本身和几个博客帖子足够丰富的话题。 另外,在Odersky,Spoon和Venner的书“在Scala中编程”中对此进行了详细的讨论。

第五种变化:使用递归函数

值得指出另一种建立长度和上限列表的方法。 递归函数的功能,这看他们的输入,然后或者返回结果的基本情况和计算结果,然后调用自理与结果。 这是计算机科学家喜欢的非常标准的东西,并且在功能性编程中比在命令性编程中使用更多。 在这里,我将展示如何使用递归来完成相同的任务,但是没有深入的解释,因此您要么已经知道如何进行递归,就可以在Scala中针对与上面相同的问题上下文进行查看。 ,或者您对递归了解不多,但是可以将其用作如何将其用于您已经从上述优点中了解的任务的示例。 因此,在后一种情况下,希望它将与其他有关递归的教程一起使用。
首先,我们需要定义递归函数,如下所示。 它具有三个参数:一个用于单词列表,一个用于已经计算的长度,另一个用于已经计算的大写。 它返回一个对,该对首先具有长度列表和一个附加项目,然后再附加一个大写值列表和一个附加项目。 前置项是从inputWords列表的开头计算的。

def lengthCapRecursive(
  inputWords: List[String],
  lengths: List[Int],
  caps: List[Boolean]): (List[Int], List[Boolean]) = inputWords match {

  case Nil =>
    (lengths, caps)
  case head :: tail =>
    lengthCapRecursive(tail, head.length :: lengths, head(0).isUpper :: caps)
}

我们可以直接调用此函数,但是提供一个辅助函数通常很方便,该函数以空结果列表作为第二和第三参数对该函数进行初始调用。 然后,辅助功能可以执行反向操作并返回所需的计算列表。

def lengthCapRecursive(inputWords: List[String]): (List[Int], List[Boolean]) = {
val (l,c) = lengthCapRecursive(words, List[Int](), List[Boolean]())
(l.reverse, c.reverse)
}

这样就可以得到结果,只需在单词列表中调用该函数即可。

val (wlengthsRecursive, wcapsRecursive) = lengthCapRecursive(words)

display("Using a recursive function.", wlengthsRecursive, wcapsRecursive)

对此稍作更改的一种更简洁的方法是将“递归函数”“隐藏”在辅助函数内部,然后有效地充当递归函数的包装器。 通常认为这比较干净,因为程序员可以确保初始化正确完成,并且递归函数本身未提供格式错误的输入。

def lengthCapRecurWrap(inputWords: List[String]): (List[Int], List[Boolean]) = {

  // This function is hidden from code that doesn't
  def lengthCapRecurHelp(
    inputWords: List[String],
    lengths: List[Int],
    caps: List[Boolean]): (List[Int], List[Boolean]) = inputWords match {

    case Nil =>
      (lengths, caps)
    case head :: tail =>
      lengthCapRecurHelp(tail, head.length :: lengths, head(0).isUpper :: caps)
  }

  val (l,c) = lengthCapRecursive(words, List[Int](), List[Boolean]())
  (l.reverse, c.reverse)

}

val (wlengthsRecurWrap, wcapsRecurWrap) = lengthCapRecurWrap(words)

display("Using a recursive function contained in a wrapper.", wlengthsRecurWrap, wcapsRecurWrap)

结论

因此,这提供了获得相同结果的不同方法的概述,并就可能在您的代码中出现的计算考虑因素对每种解决方案的不同属性进行了一些解释,您应该意识到。

显然,有许多方法可以在Scala中完成相同的操作。 对于刚接触该语言的人来说,这可能很难,因为他们没有很好的直觉来了解在不同情况下哪种方法更好。但是,随着您变得更加精明并了解使用不同语言的成本和收益,拥有这些选择非常有价值。数据结构和不同的迭代方式。

以上代码段中的所有代码都收集在Github要点ListComputations.scala中 。 您可以将其另存为文件,并作为“ scala ListComputations.scala ”运行,以查看输出并修改代码。

参考:来自JCG合作伙伴的 Scala中序列计算结果的变化   Bcomposes博客的Jason Baldridge


翻译自: https://www.javacodegeeks.com/2012/02/variations-for-computing-results-from.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值