Каков предпочтительный способ реализации "yield" в Scala?

Я пишу код для исследования PhD и начинаю использовать Scala. Мне часто приходится обрабатывать текст. Я привык к Python, чья формулировка "yield" чрезвычайно полезна для реализации сложных итераторов в больших, часто нерегулярно структурированных текстовых файлах. Подобные конструкции существуют на других языках (например, С#) по уважительной причине.

Да, я знаю, что на этом были предыдущие потоки. Но они выглядят как взломанные (или, по крайней мере, плохо объясненные) решения, которые явно не работают хорошо и часто имеют неясные ограничения. Я хотел бы написать код примерно так:

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, потому что код итератора с выходом почти неизбежно находится внутри цикла for.

Пожалуйста, я бы предпочел не получать ни одного из следующих ответов:

  • 'yield' отстой, продолжения лучше. (Да, в общем, вы можете делать больше с продолжениями, но они хладны трудно понять, и 99% времени итератор - это все, что вам нужно или нужно. Если Scala предоставляет множество мощных инструментов, но они слишком сложны для использования на практике, язык не будет успешным.)
  • Это дубликат. (См. Мои комментарии выше.)
  • Вы должны переписать свой код с помощью потоков, продолжений, рекурсии и т.д. и т.д. (см. № 1. Я также добавлю, технически вам также не нужны петли. В этом отношении технически вы можете делать абсолютно все вам когда-либо понадобится Комбайны SKI.)
  • Ваша функция слишком длинная. Разбейте его на мелкие кусочки, и вам не понадобится "урожай". Так или иначе, вы должны сделать это в производственном коде. (Во-первых, "вам не нужно" урожайность ", в любом случае сомнительна. Во-вторых, это не производственный код. В-третьих, для обработки текста, как это, очень часто, разбивая функцию на более мелкие части, особенно когда язык заставляет делать это, потому что ему не хватает полезных конструкций - только делает код более сложным для понимания.)
  • Перепишите код с переданной функцией. (Технически, да, вы можете это сделать. Но результат больше не является итератором, а целые итераторы гораздо приятнее, чем функции цепочки. В общем, язык не должен заставлять меня пишите в неестественном стиле - конечно, создатели Scala верят в это вообще, поскольку они обеспечивают shitloads синтаксического сахара.)
  • Перепишите свой код в этом, в том или ином виде, или в другом прохладном, удивительном способе, о котором я только что подумал.

Ответы

Ответ 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
    }
  )
)

P.S. Scala продолжается; это было сделано так (реализовано путем выброса стекированных (легких) исключений):

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

но это не поможет вам, потому что Scala не имеет нужного вам урока.

Ответ 2

'yield' sucks, продолжения лучше

Собственно, Python yield является продолжением.

Что такое продолжение? Продолжение - сохранение текущей точки исполнения со всем ее состоянием, чтобы продолжить в этот момент позже. Это именно то, что Python yield, а также точно, как оно реализовано.

Я понимаю, что продолжение Python не ограничено. Я мало что знаю об этом, может быть, я ошибаюсь. Я также не знаю, каковы могут быть последствия этого.

Scala продолжение не работает во время выполнения - на самом деле есть библиотека продолжений для Java, которые работают, делая материал для байт-кода во время выполнения, который свободен от ограничений, которые имеют Scala продолжение.

Scala продолжение выполняется во время компиляции, что требует довольно много работы. Он также требует, чтобы код, который будет "продолжен", должен быть подготовлен компилятором для этого.

И вот почему для-понимания не работает. Пример:

for { x <- xs } proc(x)

Если переведен в

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

Где foreach - метод класса xs. К сожалению, класс xs был долго компилирован, поэтому его нельзя модифицировать для поддержки продолжения. В качестве побочного примечания, также почему Scala не имеет continue.

Кроме того, да, это дублированный вопрос, и, да, вы должны найти другой способ написать свой код.

Ответ 3

Ниже приведена реализация Python-подобного генератора.

Обратите внимание, что в коде ниже есть функция с именем _yield, потому что yield уже является ключевым словом в Scala, что, кстати, не имеет ничего общего с yield, которое вы знаете из Python.

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

Это работает очень хорошо, в том числе для типичного использования для циклов.

Предостережение: нам нужно помнить, что 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, и в этом случае a 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") }}}}

Опять же, не углубляясь в сложную теорию, дело в том, что после a 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)
  }

Проанализируйте, что происходит здесь:

  • Наша функция-генератор 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)
}