Scala - может ли предложение for-yield ничего не дать для какого-то условия?

На языке Scala я хочу написать функцию, которая возвращает нечетные числа в заданном диапазоне. Функция печатает некоторый журнал при повторении четных чисел. Первая версия функции:

def getOdds(N: Int): Traversable[Int] = {
  val list = new mutable.MutableList[Int]
  for (n <- 0 until N) {
    if (n % 2 == 1) {
      list += n
    } else {
      println("skip even number " + n)
    }
  }
  return list
}

Если я не печатаю журналы, реализация становится очень простой:

def getOddsWithoutPrint(N: Int) =
  for (n <- 0 until N if (n % 2 == 1)) yield n

Тем не менее, я не хочу пропустить часть регистрации. Как переписать первую версию более компактно? Было бы здорово, если бы его можно было переписать примерно так:

def IWantToDoSomethingSimilar(N: Int) =
  for (n <- 0 until N) if (n % 2 == 1) yield n else println("skip even number " + n)

person pocorall    schedule 16.09.2012    source источник


Ответы (4)


def IWantToDoSomethingSimilar(N: Int) = 
  for {
    n <- 0 until N
    if n % 2 != 0 || { println("skip even number " + n); false }
  } yield n

Однако использование выражения filter вместо выражения for было бы немного проще.

person Luigi Plinge    schedule 16.09.2012

Если вы хотите сохранить последовательность ваших признаков (обработка шансов и четных по порядку, а не отдельно), вы можете использовать что-то вроде этого (отредактировано):

def IWantToDoSomethingSimilar(N: Int) =
  (for (n <- (0 until N)) yield {
    if (n % 2 == 1) {
        Option(n) 
    } else {
        println("skip even number " + n)
        None
    }
  // Flatten transforms the Seq[Option[Int]] into Seq[Int]
  }).flatten

РЕДАКТИРОВАТЬ, следуя той же концепции, более короткое решение:

def IWantToDoSomethingSimilar(N: Int) = 
    (0 until N) map {
        case n if n % 2 == 0 => println("skip even number "+ n)
        case n => n
    } collect {case i:Int => i}
person jwinandy    schedule 16.09.2012
comment
и вместо для понимания и сглаживания вы могли бы использовать flatMap для этого дерьма... - person Kim Stebel; 16.09.2012
comment
Спасибо! Я не думал о Option и None. К сожалению, код сокращает всего две строки, кроме комментариев. - person pocorall; 16.09.2012
comment
Почему List лучше, чем... хм, (от 0 до N)? Не могли бы вы дать мне ссылку на обучение об этом? - person pocorall; 16.09.2012
comment
@pocorall, это хороший вопрос. Когда я публиковал код, я думал, что Seq[Option[_]] нельзя сгладить, но на самом деле это возможно. Здесь нет смысла использовать toList. Извините за ФУД. - person jwinandy; 16.09.2012

Если вы хотите углубиться в функциональный подход, неплохо начать с чего-то вроде следующего.

Сначала несколько общих определений:

    // use scalaz 7
    import scalaz._, Scalaz._

    // transforms a function returning either E or B into a 
    // function returning an optional B and optionally writing a log of type E
    def logged[A, E, B, F[_]](f: A => E \/ B)(
      implicit FM: Monoid[F[E]], FP: Pointed[F]): (A => Writer[F[E], Option[B]]) = 
      (a: A) => f(a).fold(
        e => Writer(FP.point(e), None), 
        b => Writer(FM.zero, Some(b)))

    // helper for fixing the log storage format to List
    def listLogged[A, E, B](f: A => E \/ B) = logged[A, E, B, List](f)

    // shorthand for a String logger with List storage
    type W[+A] = Writer[List[String], A]

Теперь все, что вам нужно сделать, это написать свою функцию фильтрации:

    def keepOdd(n: Int): String \/ Int = 
      if (n % 2 == 1) \/.right(n) else \/.left(n + " was even")

Вы можете попробовать это прямо сейчас:

    scala> List(5, 6) map(keepOdd)
    res0: List[scalaz.\/[String,Int]] = List(\/-(5), -\/(6 was even))

Затем вы можете использовать функцию traverse, чтобы применить вашу функцию к списку входных данных и собрать как записанные журналы, так и результаты:

    scala> val x = List(5, 6).traverse[W, Option[Int]](listLogged(keepOdd))
    x: W[List[Option[Int]]] = scalaz.WriterTFunctions$$anon$26@503d0400

    // unwrap the results
    scala> x.run
    res11: (List[String], List[Option[Int]]) = (List(6 was even),List(Some(5), None))

    // we may even drop the None-s from the output
    scala> val (logs, results) = x.map(_.flatten).run
    logs: List[String] = List(6 was even)
    results: List[Int] = List(5)
person ron    schedule 16.09.2012
comment
Боже, отличный ответ! Я новичок в скалазе. Чтобы понять плюсы этого подхода, я должен сначала изучить scalaz. - person pocorall; 16.09.2012
comment
Покоралл: Получайте удовольствие! Это может помочь начать работу. stackoverflow.com/questions/4863671/good-scalaz-introduction - person ron; 16.09.2012

Я не думаю, что это можно легко сделать с помощью for comprehension. Но вы можете использовать раздел.

def getOffs(N:Int) = {
  val (evens, odds) =  0 until N partition { x => x % 2 == 0 } 
  evens foreach { x => println("skipping " + x) }
  odds
}

РЕДАКТИРОВАТЬ: Чтобы избежать печати сообщений журнала после завершения разбиения, вы можете изменить первую строку метода следующим образом:

val (evens, odds) = (0 until N).view.partition { x => x % 2 == 0 }
person Kim Stebel    schedule 16.09.2012
comment
Здорово! Как упоминал @jwinandy, журнал печатается после итерации. Это может быть проблемой, если время выполнения в цикле велико. Однако я думаю, что это был бы самый чистый код для общего случая. - person pocorall; 16.09.2012
comment
На самом деле, моя проблема заключается в реализации веб-краулера и возвращает URL-адреса, соответствующие некоторому критерию. Таким образом, фактический код, заменяющий код примера (x % 2), требует больше времени для выполнения (поскольку он обращается к веб-странице). Когда я использую SeqView, часть критерия запускается повторно (в моем случае она обращается к веб-странице при каждом доступе к объекту «шансы»). - person pocorall; 16.09.2012