Каков предпочтительный способ реализации «yield» в Scala? - PullRequest
19 голосов
/ 05 сентября 2011

Я пишу код для исследования PhD и начинаю использовать 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. 'yield' отстой, продолжения лучше.(Да, в общем, вы можете делать больше с продолжениями. Но их трудно понять, и 99% времени - итератор - это все, что вам нужно или нужно. Если Scala предоставляет множество мощных инструментов, но их слишком сложно использоватьна практике язык не будет успешным.)
  2. Это дубликат.(Пожалуйста, смотрите мои комментарии выше.)
  3. Вы должны переписать свой код, используя потоки, продолжения, рекурсию и т. Д. И т. Д. (Пожалуйста, смотрите # 1. Я также добавлю, технически вам также не нужны циклы)В этом отношении технически вы можете делать абсолютно все, что вам когда-либо нужно, используя комбинаторы SKI .)
  4. Ваша функция слишком длинная.Разбейте его на более мелкие кусочки, и вам не понадобится «доходность».В любом случае вам придется делать это в рабочем коде.(Во-первых, «вам не понадобится« yield »» в любом случае сомнительно. Во-вторых, это не рабочий код. В-третьих, для такой обработки текста очень часто разбивают функцию на более мелкие фрагменты - особенно когдаязык заставляет сделать это, потому что в нем отсутствуют полезные конструкции - только делает код труднее понять.)
  5. Переписать код с помощью переданной функции.(Технически, да, вы можете сделать это. Но результат больше не является итератором, и цепочка итераторов гораздо приятнее, чем функции цепочки. В общем, язык не должен заставлять меня писать в неестественном стиле - конечно, создатели Scalaв общем, поверьте, так как они обеспечивают кучу синтаксического сахара.)
  6. Перепишите ваш код тем или иным способом, или каким-нибудь другим классным, удивительным способом, о котором я только что подумал.

Ответы [ 3 ]

28 голосов
/ 05 сентября 2011

Предположительно, ваш вопрос заключается в том, что вы хотите точно получить доходность Python, и вы не хотите, чтобы другие разумные предложения делали то же самое по-другому в Scala. Если это правда, и это так важно для вас, почему бы не использовать Python? Это довольно хороший язык. Если только ваш доктор философии в области компьютерных наук, и использование Scala является важной частью вашей диссертации, если вы уже знакомы с Python и действительно любите некоторые его функции и варианты дизайна, почему бы не использовать его вместо этого?

В любом случае, если вы действительно хотите узнать, как решить вашу проблему в Scala, оказывается, что для кода, который вы имеете, продолжения с разделителями являются излишними. Все, что вам нужно, это плоские итераторы.

Вот как ты это делаешь.

// 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 нет той доходности, которую вы хотите.

16 голосов
/ 05 сентября 2011

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

На самом деле Python yield является продолжением.

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

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

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

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

И именно поэтому фор-постижения не работают. Утверждение как это:

for { x <- xs } proc(x)

Если перевести на

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

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

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

5 голосов
/ 01 июля 2012

В приведенной ниже реализации представлен 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) }
  }
}

Он работает довольно хорошо, в том числе для типичного использования циклов 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)
  }

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

  1. Наша функция генератора myGenerator содержит некоторую логику, которая генерирует информацию.В этом примере мы просто используем последовательность строк.

  2. Наша генераторная функция myGenerator вызывает рекурсивную функцию, которая отвечает за yield -изображение нескольких фрагментов информации, полученных изнаша последовательность строк.

  3. Рекурсивная функция должна быть объявлена ​​перед использованием , в противном случае компилятор падает.

  4. рекурсивная функция tailrec обеспечивает необходимую нам хвостовую рекурсию.

Основное правило здесь простое: замените цикл на рекурсивную функцию, как показано выше.

Обратите внимание, что 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)
} 
...