[英]What is the preferred way to implement 'yield' in Scala?
我正在为博士研究编写代码并开始使用Scala。 我经常要做文字处理。 我已经习惯了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()的代码。 BTW give()应命名为yield(),但Scala已经使用了该关键字。
我认为,由于我不理解的原因,Scala延续可能不适用于for语句。 如果是这样,generate()应该提供一个尽可能接近for语句的等效函数,因为带有yield的迭代器代码几乎不可避免地位于for循环中。
请,我不希望得到以下任何答案:
你的问题的前提似乎是你想要Python的收益率,并且你不希望任何其他合理的建议在Scala中以不同的方式做同样的事情。 如果这是真的,那对你来说很重要,为什么不使用Python呢? 这是一个很好的语言。 除非你的博士学位 是在计算机科学,使用Scala是你论文的重要部分,如果你已经熟悉Python并且非常喜欢它的一些功能和设计选择,为什么不使用它呢?
无论如何,如果你真的想学习如何在Scala中解决你的问题,事实证明,对于你所拥有的代码,分隔的延续是过度的。 您只需要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
}
)
)
PS Scala 确实有继续; 它是这样完成的(通过抛出无堆栈(轻量级)异常实现):
import scala.util.control.Breaks._
for (blah) { breakable { ... break ... } }
但这不会得到你想要的东西,因为Scala没有你想要的产量。
'收益'很糟糕,延续更好
实际上,Python的yield
是一个延续。
什么是延续? 延续是将当前执行点与其所有状态保存在一起,以便稍后可以继续执行。 这正是Python的yield
,以及它的实现方式。
但是我的理解是,Python的延续不是分隔的 。 我对此并不了解 - 实际上我可能错了。 我也不知道这可能是什么影响。
Scala的延续在运行时不起作用 - 实际上,有一个Java的延续库,它通过在运行时对字节码进行操作来完成,这不受Scala延续的约束。
Scala的延续完全在编译时完成,这需要相当多的工作。 它还要求编译器准备“继续”的代码。
这就是为什么理解不起作用的原因。 这样的陈述:
for { x <- xs } proc(x)
如果翻译成
xs.foreach(x => proc(x))
foreach
是xs
类的一种方法。 不幸的是, xs
类已被长期编译,因此无法修改为支持延续。 作为旁注,这也是Scala没有continue
。
除此之外,是的,这是一个重复的问题,是的,您应该找到一种不同的方式来编写代码。
下面的实现提供了类似Python的生成器。
请注意,下面的代码中有一个名为_yield
的函数,因为yield
已经是Scala中的一个关键字,顺便说一句,它与您从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在实现continuation的方式上有所不同。 下面我们看看如何在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
s的顺序进行分解 ,在这种情况下, yield
与continuation中涉及的类型匹配。 更准确地说,代码可以分解为链接块,其中每个块以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
结束,依此类推。 显然这件事必须在某个时候结束。 那么我们唯一允许做的就是关闭整个链条。
好。 但是......我们如何才能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
-ing多条信息,从我们的字符串的序列获得。
必须在使用前声明递归函数,否则编译器崩溃。
递归函数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 continuation实现yield(yield return) *
致Miles Sabin的信用,将上面的一些代码放在一起
http://github.com/milessabin/scala-cont-jvm-coro-talk/blob/master/src/continuations/Generators.scala
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.