在Scala中实现'yield'的最佳方式是什么?
我正在为我的博士研究写代码,最近开始使用Scala。因为我经常需要处理文本,所以我习惯用Python,Python里的'yield'语句非常好用,可以帮助我处理大型、结构不太规则的文本文件,特别是在实现复杂的迭代器时。其他语言(比如C#)也有类似的功能,这也是有原因的。
我知道之前有讨论过这个问题,但那些讨论里的解决方案看起来都不太靠谱,或者解释得很糟糕,效果也不明显,限制条件也不清楚。我想写的代码大概是这样的:
import generator._
def yield_values(file:String) = {
generate {
for (x <- Source.fromFile(file).getLines()) {
# Scala is already using the 'yield' keyword.
give("something")
for (field <- ":".r.split(x)) {
if (field contains "/") {
for (subfield <- "/".r.split(field)) { give(subfield) }
} else {
// Scala has no 'continue'. IMO that should be considered
// a bug in Scala.
// Preferred: if (field.startsWith("#")) continue
// Actual: Need to indent all following code
if (!field.startsWith("#")) {
val some_calculation = { ... do some more stuff here ... }
if (some_calculation && field.startsWith("r")) {
give("r")
give(field.slice(1))
} else {
// Typically there will be a good deal more code here to handle different cases
give(field)
}
}
}
}
}
}
}
我希望能看到实现generate()和give()的代码。顺便说一下,give()应该叫yield(),但Scala已经把这个关键词用了。
我了解到,由于某些我不太明白的原因,Scala的继续(continuations)可能在for语句里不太好用。如果真是这样,generate()应该提供一个尽量接近for语句的功能,因为用yield的迭代器代码几乎总是放在for循环里。
请不要给我以下这些答案:
- 'yield'不好,继续更好。(是的,通常来说,继续可以做更多事情。但它们真的很难理解,而且99%的情况下,迭代器就是你想要或需要的。如果Scala提供了很多强大的工具,但用起来太复杂,那这个语言就不会成功。)
- 这是重复的问题。(请看我上面的评论。)
- 你应该用流、继续、递归等重写你的代码。(请看第1条。我还想补充一点,技术上你也不需要for循环。其实,你可以用SKI组合子做任何你需要的事情。)
- 你的函数太长了,拆分成小块就不需要'yield'了。反正你在生产代码里也得这么做。(首先,“你不需要'yield'”在任何情况下都不一定成立。其次,这不是生产代码。第三,对于这种文本处理,通常来说,拆分函数成小块——尤其是在语言强制你这么做,因为它缺少有用的构造——只会让代码变得更难理解。)
- 用传入的函数重写你的代码。(技术上,是的,你可以这么做。但结果就不再是迭代器了,链式调用迭代器比链式调用函数要好得多。一般来说,语言不应该强迫我以不自然的方式编写代码——当然,Scala的创造者们也认为这一点,因为他们提供了很多语法糖。)
- 用这种、那种或者其他我刚想到的酷炫方法重写你的代码。
3 个回答
下面的实现提供了一种类似Python的生成器。
注意,下面的代码中有一个叫做 _yield
的函数,因为在Scala中 yield
已经是一个关键字,顺便说一下,它和你在Python中知道的 yield
没有关系。
import scala.annotation.tailrec
import scala.collection.immutable.Stream
import scala.util.continuations._
object Generators {
sealed trait Trampoline[+T]
case object Done extends Trampoline[Nothing]
case class Continue[T](result: T, next: Unit => Trampoline[T]) extends Trampoline[T]
class Generator[T](var cont: Unit => Trampoline[T]) extends Iterator[T] {
def next: T = {
cont() match {
case Continue(r, nextCont) => cont = nextCont; r
case _ => sys.error("Generator exhausted")
}
}
def hasNext = cont() != Done
}
type Gen[T] = cps[Trampoline[T]]
def generator[T](body: => Unit @Gen[T]): Generator[T] = {
new Generator((Unit) => reset { body; Done })
}
def _yield[T](t: T): Unit @Gen[T] =
shift { (cont: Unit => Trampoline[T]) => Continue(t, cont) }
}
object TestCase {
import Generators._
def sectors = generator {
def tailrec(seq: Seq[String]): Unit @Gen[String] = {
if (!seq.isEmpty) {
_yield(seq.head)
tailrec(seq.tail)
}
}
val list: Seq[String] = List("Financials", "Materials", "Technology", "Utilities")
tailrec(list)
}
def main(args: Array[String]): Unit = {
for (s <- sectors) { println(s) }
}
}
这个实现效果很好,包括在常见的for循环中的使用。
需要注意的是:我们要记住Python和Scala在实现继续执行的方式上是不同的。下面我们将看到生成器在Python中的典型用法,并与Scala中的用法进行比较。然后,我们会看到为什么在Scala中需要这样做。
如果你习惯用Python写代码,你可能会这样使用生成器:
// This is Scala code that does not compile :(
// This code naively tries to mimic the way generators are used in Python
def myGenerator = generator {
val list: Seq[String] = List("Financials", "Materials", "Technology", "Utilities")
list foreach {s => _yield(s)}
}
上面的代码无法编译。跳过所有复杂的理论,简单来说:它无法编译是因为 "for循环的类型" 和作为继续执行部分的类型不匹配。抱歉,这个解释完全失败了。让我再试一次:
如果你写了下面这样的代码,它就能正常编译:
def myGenerator = generator {
_yield("Financials")
_yield("Materials")
_yield("Technology")
_yield("Utilities")
}
这段代码能编译通过,因为生成器可以被 分解 成一系列的 yield
,在这种情况下,一个 yield
与继续执行的类型匹配。更准确地说,代码可以被分解成链式的块,每个块以 yield
结束。为了更清楚地说明,我们可以认为 yield
的序列可以这样表示:
{ some code here; _yield("Financials")
{ some other code here; _yield("Materials")
{ eventually even some more code here; _yield("Technology")
{ ok, fine, youve got the idea, right?; _yield("Utilities") }}}}
再次强调,不深入复杂的理论,关键是,在一个 yield
之后,你需要提供另一个以 yield
结束的块,或者以其他方式结束这个链。这就是我们在上面的伪代码中所做的:在 yield
之后,我们打开另一个块,这个块又以 yield
结束,接着又是一个 yield
,然后又以另一个 yield
结束,依此类推。显然,这个过程必须在某个时刻结束。然后我们唯一能做的就是关闭整个链。
好的。但是...我们如何能 yield
多个信息呢?答案有点模糊,但在你知道答案后会很有道理:我们需要使用尾递归,并且一个块的最后一条语句必须是 yield
。
def myGenerator = generator {
def tailrec(seq: Seq[String]): Unit @Gen[String] = {
if (!seq.isEmpty) {
_yield(seq.head)
tailrec(seq.tail)
}
}
val list = List("Financials", "Materials", "Technology", "Utilities")
tailrec(list)
}
让我们分析一下这里发生了什么:
我们的生成器函数
myGenerator
包含一些逻辑来生成信息。在这个例子中,我们简单地使用了一系列字符串。我们的生成器函数
myGenerator
调用一个递归函数,这个函数负责yield
多个信息,这些信息来自我们的字符串序列。递归函数 必须在使用之前声明,否则编译器会崩溃。
递归函数
tailrec
提供了我们需要的尾递归。
这里的经验法则很简单:用递归函数替代for循环,如上所示。
注意,tailrec
只是我们找到的一个方便的名称,为了更清楚。特别是,tailrec
不一定是我们生成器函数的最后一条语句;不一定。唯一的限制是你必须提供一系列与 yield
类型匹配的块,如下所示:
def myGenerator = generator {
def tailrec(seq: Seq[String]): Unit @Gen[String] = {
if (!seq.isEmpty) {
_yield(seq.head)
tailrec(seq.tail)
}
}
_yield("Before the first call")
_yield("OK... not yet...")
_yield("Ready... steady... go")
val list = List("Financials", "Materials", "Technology", "Utilities")
tailrec(list)
_yield("done")
_yield("long life and prosperity")
}
进一步说,你一定在想实际应用是怎样的,特别是如果你使用多个生成器的话。如果你能找到一种方法来 标准化 你的生成器,使其围绕一个单一的模式,这在大多数情况下会很方便,那将是个好主意。
让我们看看下面的例子。我们有三个生成器: sectors
、 industries
和 companies
。为了简洁起见,只有 sectors
完整显示。这个生成器使用了一个 tailrec
函数,如上面已经演示过的。这里的窍门是,其他生成器也使用相同的 tailrec
函数。我们所要做的就是提供一个不同的 body
函数。
type GenP = (NodeSeq, NodeSeq, NodeSeq)
type GenR = immutable.Map[String, String]
def tailrec(p: GenP)(body: GenP => GenR): Unit @Gen[GenR] = {
val (stats, rows, header) = p
if (!stats.isEmpty && !rows.isEmpty) {
val heads: GenP = (stats.head, rows.head, header)
val tails: GenP = (stats.tail, rows.tail, header)
_yield(body(heads))
// tail recursion
tailrec(tails)(body)
}
}
def sectors = generator[GenR] {
def body(p: GenP): GenR = {
// unpack arguments
val stat, row, header = p
// obtain name and url
val name = (row \ "a").text
val url = (row \ "a" \ "@href").text
// create map and populate fields: name and url
var m = new scala.collection.mutable.HashMap[String, String]
m.put("name", name)
m.put("url", url)
// populate other fields
(header, stat).zipped.foreach { (k, v) => m.put(k.text, v.text) }
// returns a map
m
}
val root : scala.xml.NodeSeq = cache.loadHTML5(urlSectors) // obtain entire page
val header: scala.xml.NodeSeq = ... // code is omitted
val stats : scala.xml.NodeSeq = ... // code is omitted
val rows : scala.xml.NodeSeq = ... // code is omitted
// tail recursion
tailrec((stats, rows, header))(body)
}
def industries(sector: String) = generator[GenR] {
def body(p: GenP): GenR = {
//++ similar to 'body' demonstrated in "sectors"
// returns a map
m
}
//++ obtain NodeSeq variables, like demonstrated in "sectors"
// tail recursion
tailrec((stats, rows, header))(body)
}
def companies(sector: String) = generator[GenR] {
def body(p: GenP): GenR = {
//++ similar to 'body' demonstrated in "sectors"
// returns a map
m
}
//++ obtain NodeSeq variables, like demonstrated in "sectors"
// tail recursion
tailrec((stats, rows, header))(body)
}
感谢 Rich Dougherty 和 huynhjl。
查看这个SO讨论: 使用Scala继续实现yield(yield return)*感谢 Miles Sabin,整理了上面的部分代码
http://github.com/milessabin/scala-cont-jvm-coro-talk/blob/master/src/continuations/Generators.scala
'yield' 不好用,继续执行的方式更好
其实,Python 的 yield
实际上就是一种继续执行的方式。
什么是继续执行?简单来说,就是保存当前执行的位置和所有相关的状态,这样你就可以稍后从这个位置继续执行。这正是 Python 的 yield
所做的,也是它的实现方式。
不过,我了解到 Python 的继续执行并不是有“界限”的。我对这个了解不多,可能我说错了。而且我也不清楚这会有什么影响。
Scala 的继续执行在运行时是不能用的——实际上,有一个 Java 的继续执行库,它通过在运行时处理字节码来实现,这样就不受 Scala 的继续执行限制。
Scala 的继续执行完全是在编译时完成的,这需要做很多工作。而且需要编译器准备好将要“继续”的代码。
这就是为什么 for-comprehensions 不好用的原因。像这样的语句:
for { x <- xs } proc(x)
如果翻译成
xs.foreach(x => proc(x))
其中 foreach
是 xs
类中的一个方法。不幸的是,xs
类早已被编译,所以无法修改以支持继续执行。顺便提一下,这也是 Scala 没有 continue
的原因。
除此之外,是的,这个问题是重复的,没错,你应该找到其他方式来写你的代码。
你的问题似乎是想要完全一样的Python的yield功能,而不想听其他在Scala中实现相似功能的合理建议。如果真是这样,而且这对你来说非常重要,那为什么不直接用Python呢?Python是一门很不错的语言。除非你的博士学位是计算机科学,并且使用Scala是你论文的重要部分,否则如果你已经熟悉Python,并且真的喜欢它的一些特性和设计选择,那为什么不直接用它呢?
不过,如果你真的想学会如何在Scala中解决你的问题,实际上,对于你写的代码来说,使用分隔的继续(delimited continuations)是过于复杂的。你只需要使用flatMapped迭代器就可以了。
下面是怎么做的。
// You want to write
for (x <- xs) { /* complex yield in here */ }
// Instead you write
xs.iterator.flatMap { /* Produce iterators in here */ }
// You want to write
yield(a)
yield(b)
// Instead you write
Iterator(a,b)
// You want to write
yield(a)
/* complex set of yields in here */
// Instead you write
Iterator(a) ++ /* produce complex iterator here */
就这样!你所有的情况都可以简化为这三种之一。
在你的例子中,代码看起来会像这样:
Source.fromFile(file).getLines().flatMap(x =>
Iterator("something") ++
":".r.split(x).iterator.flatMap(field =>
if (field contains "/") "/".r.split(field).iterator
else {
if (!field.startsWith("#")) {
/* vals, whatever */
if (some_calculation && field.startsWith("r")) Iterator("r",field.slice(1))
else Iterator(field)
}
else Iterator.empty
}
)
)
顺便说一下,Scala确实有continue;它是这样实现的(通过抛出无栈(轻量级)异常):
import scala.util.control.Breaks._
for (blah) { breakable { ... break ... } }
但是这并不能满足你的需求,因为Scala没有你想要的yield功能。