Каков предпочтительный способ реализации "yield" в Scala?
Я пишу код для исследований 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(). BTW give() должен быть назван yield (), но Scala уже взял это ключевое слово.
Я понимаю, что по причинам, которые я не понимаю, продолжения Scala могут не работать внутри оператора for. Если это так, generate() должен предоставить эквивалентную функцию, которая работает как можно ближе к A for оператор, потому что код итератора с выходом почти неизбежно сидит внутри цикла for.
пожалуйста, я бы предпочел не получать ни одного из следующих ответов:
- 'yield' отстой, продолжения лучше. (Да, в общем, вы можете сделать больше с продолжениями. Но их чертовски трудно понять, и 99% времени итератор-это все, что вам нужно или нужно. Если Scala предоставляет множество мощных инструментов, но их слишком сложно использовать на практике, язык не будет преуспевать.)
- это дубликат. (См. мои комментарии выше.)
- вы должны переписать свой код, используя потоки, продолжения, рекурсию и т. д. так далее. (Пожалуйста, см. #1. Я также добавлю, технически вам также не нужны петли. Если на то пошло, технически вы можете сделать абсолютно все, что вам когда-либо нужно, используя лыжные двоеборцы.)
- ваша функция слишком долго. Разбейте его на более мелкие кусочки, и вам не понадобится "выход". Тебе придется сделайте это в производственном коде, во всяком случае. (Во-первых, "вам не нужно будет" уступать "" в любом случае сомнительно. Во-вторых, это не производственный код. В-третьих, для обработки текста, как это, очень часто, разбивая функцию на более мелкие части - особенно, когда язык сил вы делаете это, потому что ему не хватает полезных конструкций-только делает код сильнее чтобы понять.)
- перепишите код с помощью переданной функции. (Технически, да, вы можете это сделать. Но результат больше не является итератором, а цепные итераторы намного лучше, чем цепные функции. В общем, язык не должен заставлять меня писать в неестественном стиле - конечно, создатели Scala верят в это в целом, поскольку они обеспечивают дерьмо синтаксического сахара.)
- перепишите код в этом, тем или другим путем, или другой классный способ я только что придумал.
3 ответов
предпосылка вашего вопроса, похоже, заключается в том, что вы хотите точно получить доходность 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
}
)
)
П. С. Скала тут есть продолжайте; это сделано так (реализовано путем бросания Stackless (легких) исключений):
import scala.util.control.Breaks._
for (blah) { breakable { ... break ... } }
но это не даст вам то, что вы хотите, потому что Scala не имеет желаемого результата.
'yield' отстой, продолжения лучше
на самом деле, в Python yield
is продолжение.
какое продолжение? Продолжение сохраняет текущую точку исполнения со всем ее состоянием, таким, что можно дальше в этот момент позже. Это именно то, что Python yield
, а также, как именно это реализуется.
насколько я понимаю, продолжения Python не разделителями однако. Я мало что об этом знаю-я могу ошибаться. И я не знаю, что это может означать.
продолжение Scala не работает во время выполнения-на самом деле, есть библиотека продолжений для Java, которая работает, делая материал для байт-кода во время выполнения, который свободен от ограничений, которые имеют продолжение Scala.
продолжение Scala полностью выполняется во время компиляции, что требует довольно много работы. Он также требуется, чтобы код, который будет "продолжен", был подготовлен компилятором для этого.
и вот почему-генераторы не работают. Такое утверждение:
for { x <- xs } proc(x)
если перевести на
xs.foreach(x => proc(x))
здесь foreach
метод 'ы. К сожалению, xs
класс был давно скомпилирован, поэтому его нельзя изменить в поддержку продолжения. В качестве примечания, вот почему Scala не имеет continue
.
помимо этого, да, это дублирующий вопрос, и, да, вы должны найти другой способ написать свой код.
реализация ниже предоставляет генератор, подобный 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
s и, в данном случае, a 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
и так далее. Очевидно, все это когда-нибудь закончится. Тогда единственное, что нам позволено сделать, это закрыть всю цепочку.
ОК. Но... как мы можем!--8--> несколько фрагментов информации? Ответ немного неясен, но имеет смысл после того, как вы знаете ответ: нам нужно использовать хвостовую рекурсию, и последнее утверждение блока должно быть 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")
}
еще один шаг, вы должны представить, как выглядят приложения в реальной жизни, в частности, если вы используете несколько генераторов. Было бы неплохо, если бы вы нашли способ стандартизация ваши генераторы вокруг одного шаблона, который демонстрирует, чтобы быть удобным для большинства обстоятельств.
давайте рассмотрим пример ниже. У нас три генератора:--29-->, industries
и companies
. Для краткости, только sectors
полностью показано. Этот генератор использует tailrec
функция, как показано выше. Фокус здесь в том, что то же самое tailrec
функция также использована другими генераторами. Все, что нам нужно сделать, это поставить другой