在 Scala 中实现'yield'的首选方法是什么?



我正在为博士研究编写代码,并开始使用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()的代码。顺便说一句,give()应该被命名为yield(),但是Scala已经使用了这个关键字。

我认为,由于我不理解的原因,Scala的延续可能无法在for语句中工作。如果是这样,generate()应该提供一个尽可能接近for语句的等效函数,因为带有yield的迭代器代码几乎不可避免地位于for循环中。

拜托,我不希望得到以下任何一个答案:

  1. "产量"很糟糕,延续更好。(是的,一般来说,使用延续可以做更多的事情。但它们非常难以理解,99%的情况下,迭代器就是你想要或需要的。如果Scala提供了很多强大的工具,但是在实践中很难使用,那么这个语言就不会成功。
  2. 这是一个副本。(请参阅我上面的评论)你应该使用流、延续、递归等等来重写你的代码(请参见#1)。我还要补充一点,从技术上讲,你也不需要for循环。就此而言,从技术上讲,你可以使用SKI组合器做任何你需要的事情。)你的函数太长了。把它分成更小的部分,你就不需要"屈服"了。无论如何,您必须在生产代码中执行此操作。(首先,"你不需要‘屈服’"在任何情况下都是值得怀疑的。其次,这不是产品代码。第三,对于这样的文本处理,通常将函数分解成更小的部分——特别是当语言因为缺乏有用的结构而强迫您这样做时——只会使代码更难理解。用传入的函数重写你的代码。(从技术上讲,你可以这样做。但是结果不再是一个迭代器,而链接迭代器比链接函数要好得多。一般来说,一门语言不应该强迫我用一种不自然的风格来编写——当然,Scala的创建者通常相信这一点,因为他们提供了大量的语法糖。
  3. 用这个,那个,或者其他方式重写你的代码,或者其他一些我刚想到的很酷,很棒的方式。

你的问题的前提似乎是你完全想要Python的yield,而你不希望任何其他合理的建议在Scala中以不同的方式做同样的事情。如果这是真的,而且对你来说很重要,为什么不使用Python呢?这是一门很好的语言。除非你的博士学位是计算机科学,并且使用Scala是你论文的重要组成部分,否则如果你已经熟悉Python,并且真的很喜欢它的一些特性和设计选择,为什么不使用它呢?

无论如何,如果你真的想学习如何在Scala中解决你的问题,事实证明,对于你拥有的代码,分隔的延续是多余的。你所需要的只是flatmap迭代器。

你可以这样做。

// 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

yield很烂,延续更好

实际上,Python的yield 的延续。

什么是延续?延续保存当前执行点及其所有状态,以便稍后可以在该点继续。这正是Python的yield,也正是它是如何实现的。

然而,我的理解是Python的延续不是分隔的。我对此了解不多——事实上,我可能错了。我也不知道这可能意味着什么。

Scala的延续不能在运行时工作——事实上,Java有一个延续库,它通过在运行时对字节码做一些事情来工作,它不受Scala延续所具有的限制。

Scala的延续完全在编译时完成,这需要相当多的工作。它还要求编译器准备好要"继续"的代码。

这就是为什么for-comprehensions不起作用。像这样的语句:

for { x <- xs } proc(x)

如果翻译成

xs.foreach(x => proc(x))

其中foreachxs类的方法。不幸的是,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与延续中涉及的类型匹配。更精确地说,代码可以分解成链接的块,每个块以yield结束。为了澄清起见,我们可以认为yield s的序列可以这样表示:

{ 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结束,以此类推。很明显,这事总有结束的时候。那么我们唯一能做的就是关闭整个链条。

OK。但是…我们如何才能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)
  }

让我们分析一下这里发生了什么:

  1. 我们的生成器函数myGenerator包含一些获取生成信息的逻辑。在本例中,我们简单地使用了一个字符串序列。

  2. 我们的生成器函数myGenerator调用一个递归函数,该递归函数负责yield从字符串序列中获得多条信息。

  3. 递归函数必须在使用之前声明,否则编译器会崩溃。

  4. 递归函数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, industriescompanies。为简洁起见,只完整地显示了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。
    使用Scala continuation实现yield (yield return) *

  • 感谢Miles Sabin,将上面的一些代码放在一起
    http://github.com/milessabin/scala-cont-jvm-coro-talk/blob/master/src/continuations/Generators.scala

相关内容

最新更新