【问题标题】:What is the preferred way to implement 'yield' in Scala?在 Scala 中实现“yield”的首选方法是什么?
【发布时间】:2011-11-10 07:35:54
【问题描述】:

我正在为博士研究编写代码并开始使用 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 循环内。

请,我不希望得到以下任何答案:

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

【问题讨论】:

  • 好问题。似乎延续将是实现这一点的自然方式。 Scala 的分隔延续是否与 for 理解不兼容(我的意思是,在公共集合上定义的像 foreach 这样的高阶方法)?如果有人能清楚地解释 Scala 延续的局限性,那就太好了。
  • -1 拒绝“以这种方式重写你的代码”。总的来说,答案是态度差,尤其不是很聪明。
  • 不知何故你的代码看起来像一个解析器。您是否考虑过使用解析器组合器?
  • 看起来你的“态度”刚刚让你投了 3 票,但我倾向于同意。

标签: python scala generator yield text-processing


【解决方案1】:

您的问题的前提似乎是您完全想要 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
    }
  )
)

附: Scala 确实继续;它是这样完成的(通过抛出无堆栈(轻量级)异常来实现):

import scala.util.control.Breaks._
for (blah) { breakable { ... break ... } }

但这不会得到你想要的,因为 Scala 没有你想要的收益。

【讨论】:

  • 优雅而简单。谢谢雷克斯!
  • 如果你有一个生成器,或者你只想继续到最里面的生成器,那 continue 工作。就我个人而言,我认为它是如此残废,以至于它的存在都不值得一提。
  • @Danial C. Sobral - 您可以创建自己的易碎物品,这样您就可以随时随地摆脱任何想要的东西。然而,它在创建迭代器方面并不能很好地发挥作用,但对于纯控制流来说,它实际上比大多数语言的中断/继续更灵活。
【解决方案2】:

'yield' 很烂,继续更好

其实Python的yield的延续。

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

据我了解,Python 的延续不是分隔。我对此知之甚少——事实上,我可能错了。我也不知道这可能意味着什么。

Scala 的 continuation 在运行时不起作用——事实上,有一个 Java 的 continuation 库通过在运行时对字节码进行处理来工作,它不受 Scala 的 continuation 的限制。

Scala 的延续完全在编译时完成,这需要相当多的工作。它还要求编译器准备“继续”的代码来执行此操作。

这就是为什么 for-comprehensions 不起作用的原因。像这样的声明:

for { x <- xs } proc(x)

如果翻译成

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

其中foreachxs 类的方法。不幸的是,xs 类已经编译很久了,所以不能修改为支持继续。附带说明一下,这也是 Scala 没有 continue 的原因。

除此之外,是的,这是一个重复的问题,而且,是的,你应该找到一种不同的方式来编写你的代码。

【讨论】:

  • 有一些基于延续的解决方案可以满足 Urban Vagabond 的需求 (stackoverflow.com/questions/2201882/… ),但它们显然不适用于现有集合中定义的 foreach。简单的解决方案是将 for 循环重写为 while 循环。但或者,是否可以将现有的 foreach 方法替换为 与延续兼容的自定义方法?
  • Tiark(Scala 延续者)有一个真的很棒的技巧,它使集合库中的几乎所有高阶函数都兼容(包括foreach)。但是,a) 它还没有落地,b) 它不是任何高阶函数的通用解决方案(它之所以有效,是因为集合库非常通用)。
  • 你有链接吗?预计何时将其合并到主干中?
  • @Daniel 这真是我想看到的一个技巧。从你说的看,好像和CBF有关?
  • @soc,这是创建与 Scala 延续兼容的高阶函数的一种尝试:Monadic Continuations in Scala
【解决方案3】:

下面的实现提供了一个类似 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 在实现延续的方式上有所不同。下面我们将看到生成器通常如何在 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")
}

此代码可以编译,因为生成器可以在yields 的序列中分解,在这种情况下,yield 匹配延续中涉及的类型。更准确地说,可以将代码分解为链式块,其中每个块以yield 结尾。为了清楚起见,我们可以认为yields的序列可以这样表示:

{ 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)
  }

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

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

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

  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")
  }

更进一步,您必须想象现实生活中的应用程序是什么样子,尤其是在您使用多个生成器的情况下。如果您找到一种方法来标准化您的生成器,这将是一个好主意,该方法可以证明在大多数情况下都很方便。

让我们看看下面的例子。我们有三个生成器:sectorsindustriescompanies。为简洁起见,仅完整显示了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)
} 

【讨论】:

  • 我是否正确,这不适用于 for(){} 循环之类的东西,因为其中的 shift{} 会导致问题?
  • 你好理查德,这是我正在寻找的,比以前的答案更多。但是,正如李浩毅所说,这是否与 Scala 的其他部分配合得很好?如果它在 for 循环中不起作用,那就不好了。
  • @UrbanVagabond :for 循环需要替换为产生给定结果类型的递归函数。我已经重写了答案,我希望现在已经足够清楚了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-27
  • 1970-01-01
相关资源
最近更新 更多